commit 9491dafcca56175ff750522c8712ab11daa2daad Author: Sunil Thakares Date: Sat May 24 23:07:23 2025 +0530 Initial commit: all files from local directory diff --git a/Algorithm-Flowchart.md b/Algorithm-Flowchart.md new file mode 100644 index 0000000..727673e --- /dev/null +++ b/Algorithm-Flowchart.md @@ -0,0 +1,146 @@ + +# DNS Server Algorithm & Flowchart + +This document outlines the algorithm and flowchart for a DNS server implementation compliant with RFC 1035 (DNS) and RFC 4034 (DNSSEC). + +--- + +## ✅ Server Algorithm + +### 1. Server Initialization + +1. Load configuration from environment variables. +2. Initialize the logging system. +3. Create SQLite database connection and initialize schema. +4. Initialize cache with NS records. +5. Start periodic cache cleanup task (every 5 minutes). +6. Bind and listen on UDP and TCP sockets. + +### 2. Query Handling Flow + +#### Upon Receiving a DNS Query: + +1. Validate DNS query packet. +2. Parse header and extract domain name and query type. +3. If query type is `DNSKEY` or `DS`, return signed records. +4. Check DNS cache: + - If **hit**, build and return response. + - If **miss**, lookup in database: + - If found, respond and cache it. + - If not found: + - If authoritative, return `NXDOMAIN`. + - Else, forward to upstream resolvers. +5. Add DNSSEC signatures if applicable. +6. Send response to the client. + +### 3. DNSSEC Signing Process + +1. Load DNSSEC key from configured file. +2. For each relevant record: + - Generate `RRSIG`. + - Encode signature (Base64). + - Calculate key tag and signature expiration. +3. Add `RRSIG` to the answer section. +4. Include `DNSKEY` in the authority section if needed. + +### 4. Response Generation Logic + +1. Construct response header: + - Set QR flag and response code. + - Include Authoritative Answer (AA) if authoritative. +2. Attach original question section. +3. Populate: + - **Answer** section: with resolved records. + - **Authority** section: with NS and DS records. + - **Additional** section: with glue records, DNSKEY if required. + +--- + +## 📊 Flowchart +![Flowchart](flow-chart.png) +Below is the visual representation of the DNS query handling logic: + +``` + ++---------------------+ +| Start DNS Server | ++---------------------+ + | + v ++---------------------+ +| Receive DNS Query | ++---------------------+ + | + v ++---------------------+ +| Parse Header and | +| Extract Domain & | +| Query Type | ++---------------------+ + | + +---------------------+ + | | + v v ++---------------------+ +---------------------+ +| Is Query Type | | Use Cache | +| DNSKEY/DS? | | | ++---------------------+ +---------------------+ + | | + Yes | | + v v ++---------------------+ +---------------------+ +| Return | | Lookup in SQLite DB | +| DNSSEC Record | | | ++---------------------+ +---------------------+ + | + v + +---------------------+ + | Is Authoritative | + | Zone? | + +---------------------+ + | + No | + v + +---------------------+ + | Return NXDOMAIN | + +---------------------+ + | + v + +---------------------+ + | Add GSSEC | + +---------------------+ + | + v + +---------------------+ + | Send Response | + +---------------------+ + | + v + +---------------------+ + | End | + +---------------------+ + +``` + + +## 🧩 Key Components + +| Component | Purpose | Details | +|----------------|----------------------------|------------------------------------------| +| `DnsCache` | DNS Response Cache | Thread-safe HashMap with TTL | +| `ServerConfig` | Server Configuration | Loaded via environment variables | +| `rusqlite` | Record Storage | SQLite database backend | +| `tokio` | Async I/O Runtime | UDP/TCP async handlers and tasks | +| `DNSSEC` | Secure DNS Signing | RSA-SHA256 with Base64-encoded keys | + +--- + +## ⚠️ Error Handling Strategy + +- Custom `DnsError` enum via `thiserror` +- Graceful shutdown via `SIGINT` +- Cache cleanup every 5 minutes +- Fallback to resolver forwarding +- Detailed logging at every stage + +--- diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..c340ed8 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +mailto: sunil@thakares.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b9ec1e5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,253 @@ +# Contributing to nx9-dns-server + +Thank you for considering contributing to nx9-dns-server! This document provides guidelines and instructions to help you contribute effectively to this project. + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [Getting Started](#getting-started) + - [Project Setup](#project-setup) + - [Development Environment](#development-environment) +- [How to Contribute](#how-to-contribute) + - [Reporting Bugs](#reporting-bugs) + - [Suggesting Enhancements](#suggesting-enhancements) + - [Code Contributions](#code-contributions) +- [Pull Request Process](#pull-request-process) +- [Style Guidelines](#style-guidelines) + - [Rust Code Style](#rust-code-style) + - [Commit Messages](#commit-messages) + - [Documentation](#documentation) +- [Priority Areas](#priority-areas) +- [Community](#community) +- [License](#license) + +## Code of Conduct + +By participating in this project, you are expected to uphold our [Code of Conduct](CODE_OF_CONDUCT.md). Please report unacceptable behavior to [project maintainers](mailto:maintainer@example.com). + +## Getting Started + +### Project Setup + +1. **Fork the repository** on GitHub +2. **Clone your fork**: + ```bash + git clone https://github.com/your-username/nx9-dns-server.git + cd nx9-dns-server + ``` +3. **Add the upstream remote**: + ```bash + git remote add upstream https://github.com/thakares/nx9-dns-server.git + ``` +4. **Create a branch** for your work: + ```bash + git checkout -b feature/your-feature-name + ``` + +### Development Environment + +#### Requirements +- Rust (stable, 1.70+) +- SQLite 3.x +- Cargo and standard Rust toolchain + +#### Setup +1. **Install dependencies**: + ```bash + # For Debian/Ubuntu + sudo apt-get install build-essential pkg-config libsqlite3-dev + + # For Fedora/RHEL + sudo dnf install gcc sqlite-devel pkgconfig + + # For macOS with Homebrew + brew install sqlite + ``` + +2. **Compile and run the project**: + ```bash + cargo build + cargo run + ``` + +3. **Run tests**: + ```bash + cargo test + ``` + +## How to Contribute + +### Reporting Bugs + +Before submitting a bug report: +- Check the [issue tracker](https://github.com/thakares/nx9-dns-server/issues) to see if the issue has already been reported +- Make sure you're using the latest version of the software +- Perform a quick search to see if the problem has already been addressed + +When submitting a bug report: +1. Use the bug report template provided +2. Include a clear and descriptive title +3. Describe the exact steps to reproduce the issue +4. Provide specific examples to demonstrate the steps +5. Describe the behavior you observed and what you expected to see +6. Include relevant logs, screenshots, or other materials +7. Mention your environment (OS, Rust version, etc.) + +### Suggesting Enhancements + +Enhancement suggestions are tracked as GitHub issues. When creating an enhancement suggestion: +1. Use the feature request template provided +2. Include a clear and descriptive title +3. Provide a detailed description of the proposed functionality +4. Explain why this enhancement would be useful to most users +5. List any alternatives you've considered +6. Include any mockups or examples if applicable + +### Code Contributions + +We're actively seeking contributions in these areas: + +1. **Web UI Development** + - Frontend components and integration with backend + - UI/UX design for DNS management + +2. **API Service** + - RESTful API implementation + - Authentication and permission handling + - Request validation + +3. **User Management** + - Authentication systems + - Role-based access control + - User onboarding flows + +4. **DNSSEC Improvements** + - Key rotation automation + - Signature verification tools + - DNSSEC validation utilities + +5. **Core DNS Improvements** + - Performance optimizations + - Additional record type support + - Protocol extensions + +6. **Documentation and Testing** + - Improving guides and examples + - Unit and integration tests + - Benchmarking tools + +## Pull Request Process + +1. **Update your fork** with the latest from upstream: + ```bash + git fetch upstream + git merge upstream/main + ``` + +2. **Implement your changes** and commit them to your feature branch + +3. **Run the test suite** to ensure your changes don't break existing functionality: + ```bash + cargo test + ``` + +4. **Add or update tests** as needed for your new functionality + +5. **Update documentation** including README.md if needed + +6. **Submit a pull request** to the main repository: + - Fill out the PR template completely + - Reference any related issues (e.g., "Fixes #123") + - Include a clear description of the changes and their motivation + - Add screenshots or terminal output if relevant + +7. **Code review process**: + - Maintainers will review your PR + - Address any requested changes or feedback + - Once approved, maintainers will merge your PR + +## Style Guidelines + +### Rust Code Style + +- Follow the [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/) +- Use `rustfmt` to format your code: + ```bash + cargo fmt + ``` +- Use `clippy` to catch common mistakes and non-idiomatic code: + ```bash + cargo clippy + ``` +- Follow the existing project style for consistency +- Use meaningful variable and function names +- Include comments for complex sections of code +- Write comprehensive documentation for public API functions + +### Commit Messages + +- Use the present tense ("Add feature" not "Added feature") +- Use the imperative mood ("Move cursor to..." not "Moves cursor to...") +- Limit the first line to 72 characters or less +- Reference issues and pull requests after the first line +- Consider using a structured format: + ``` + [Component] Short summary (up to 72 chars) + + More detailed explanation, if necessary. Wrap lines at around 72 + characters. Explain the problem this commit is solving. Focus on why + you are making this change as opposed to how. + + Fixes #123 + ``` + +### Documentation + +- Use proper grammatical sentences with punctuation +- Keep documentation up-to-date with code changes +- Include examples where appropriate +- Document all public API functions, structs, and traits +- Use Markdown formatting in doc comments and documentation files + +## Priority Areas + +We are particularly interested in contributions in these areas: + +1. **Web UI Development**: + - Creating a responsive, user-friendly interface for DNS management + - Implementing dashboard components for monitoring DNS health + - Building forms for record management with validation + +2. **API Service**: + - Implementing RESTful endpoints for DNS record CRUD operations + - Adding authentication and authorization mechanisms + - Developing batch operations for efficient record updates + +3. **User Management**: + - Building a role-based access control system + - Implementing secure authentication flows + - Creating administrative tools for user management + +4. **Documentation**: + - Improving guides and examples + - Creating API documentation + - Adding diagrams and architecture documentation + +5. **Testing**: + - Unit tests for core components + - Integration tests for end-to-end validation + - Building automated CI pipelines + +## Community + +- Join our [Discord server](https://discord.com/channels/1179651660184817714/1369586647393370253) for discussions +- Follow the project on [Twitter](https://x.com/thakares) +- Subscribe to our [mailing list](https://example.com/mailing-list) for updates + +## License + +By contributing to nx9-dns-server, you agree that your contributions will be licensed under the project's [GNU General Public License v3.0 (GPLv3)](LICENSE). + +--- + +Thank you for your interest in improving nx9-dns-server! We appreciate your time and effort in contributing to this project. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5581be9 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1340 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ff8ae62cd3a9102e5637afc8452c55acf3844001bd5374e0b0bd7b6616c038" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi 0.5.1", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libsqlite3-sys" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.3", +] + +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "metrics" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde3af1a009ed76a778cb84fdef9e7dbbdf5775ae3e4cc1f434a6a307f6f76c5" +dependencies = [ + "ahash", + "metrics-macros", + "portable-atomic", +] + +[[package]] +name = "metrics-exporter-prometheus" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d4fa7ce7c4862db464a37b0b31d89bca874562f034bd7993895572783d02950" +dependencies = [ + "base64", + "hyper", + "indexmap", + "ipnet", + "metrics", + "metrics-util", + "quanta", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "metrics-macros" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b4faf00617defe497754acde3024865bc143d44a86799b24e191ecff91354f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "metrics-util" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de2ed6e491ed114b40b732e4d1659a9d53992ebd87490c44a6ffe23739d973e" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", + "hashbrown 0.13.1", + "metrics", + "num_cpus", + "quanta", + "sketches-ddsketch", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + +[[package]] +name = "nx9-dns-server" +version = "0.1.0" +dependencies = [ + "base64", + "chrono", + "env_logger", + "hex", + "log", + "lru", + "metrics", + "metrics-exporter-prometheus", + "r2d2", + "r2d2_sqlite", + "rusqlite", + "thiserror", + "tokio", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quanta" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17e662a7a8291a865152364c20c7abc5e60486ab2001e8ec10b24862de0b9ab" +dependencies = [ + "crossbeam-utils", + "libc", + "mach2", + "once_cell", + "raw-cpuid", + "wasi 0.11.0+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + +[[package]] +name = "r2d2_sqlite" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99f31323d6161385f385046738df520e0e8694fa74852d35891fc0be08348ddc" +dependencies = [ + "r2d2", + "rusqlite", + "uuid", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + +[[package]] +name = "raw-cpuid" +version = "10.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rusqlite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" +dependencies = [ + "bitflags 2.9.1", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "sketches-ddsketch" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c" + +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + +[[package]] +name = "socket2" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "getrandom", + "js-sys", + "rand", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6753100 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +# Cargo.toml +[package] +name = "nx9-dns-server" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { version = "1.32", features = ["full"] } +rusqlite = { version = "0.29", features = ["bundled"] } +log = "0.4" +env_logger = "0.10" +thiserror = "1.0" +hex = "0.4" +base64 = "0.21" +r2d2 = "0.8" +r2d2_sqlite = "0.22" +lru = "0.12" +metrics = "0.21" +metrics-exporter-prometheus = "0.12" +chrono = "0.4" diff --git a/Docker/Dockerfile b/Docker/Dockerfile new file mode 100644 index 0000000..6ef71c8 --- /dev/null +++ b/Docker/Dockerfile @@ -0,0 +1,57 @@ +# Build stage +FROM rust:1.72-slim-bookworm AS builder + +# Install necessary build dependencies +RUN apt-get update && apt-get install -y \ + musl-tools \ + build-essential \ + pkg-config \ + libssl-dev \ + && rm -rf /var/lib/apt/lists/* + +# Add support for cross-compilation to Alpine +RUN rustup target add x86_64-unknown-linux-musl + +# Create a new empty project +WORKDIR /app +COPY . . + +# Build the project with musl target +RUN cargo build --target x86_64-unknown-linux-musl --release + +# Runtime stage +FROM alpine:3.18 + +# Install runtime dependencies +RUN apk --no-cache add ca-certificates sqlite tzdata + +# Create a non-root user for running the application +RUN addgroup -S dns && adduser -S dnsuser -G dns + +# Create necessary directories +RUN mkdir -p /var/nx9-dns-server /var/log/nx9-dns-server /etc/nx9-dns-server +RUN chown -R dnsuser:dns /var/nx9-dns-server /var/log/nx9-dns-server /etc/nx9-dns-server + +# Copy the compiled binary +COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/dns_server /usr/local/bin/ +RUN chmod +x /usr/local/bin/dns_server + +# Copy configuration files +COPY --from=builder /app/conf/dns_records.sql /etc/nx9-dns-server/ +COPY --from=builder /app/conf/dns.db.sample /etc/nx9-dns-server/ + +# Expose DNS ports +EXPOSE 53/udp 53/tcp +# Expose Web UI port +EXPOSE 8080/tcp +# Expose API port +EXPOSE 8081/tcp + +# Set working directory +WORKDIR /var/nx9-dns-server + +# Switch to non-root user +USER dnsuser + +# Command to run the application +CMD ["/usr/local/bin/dns_server"] \ No newline at end of file diff --git a/Docker/docker-compose.yml b/Docker/docker-compose.yml new file mode 100644 index 0000000..d090007 --- /dev/null +++ b/Docker/docker-compose.yml @@ -0,0 +1,24 @@ +version: '3.8' + +services: + dns: + image: nx9-dns-server:latest + container_name: nx9-dns + ports: + - "53:53/udp" + - "53:53/tcp" + - "8080:8080" + - "8081:8081" + volumes: + - ./data/dns.db:/var/nx9-dns-server/dns.db + - ./keys:/etc/nx9-dns-server/keys + - ./logs:/var/log/nx9-dns-server + environment: + - DNS_BIND=0.0.0.0:53 + - DNS_DB_PATH=/var/nx9-dns-server/dns.db + - DNSSEC_KEY_FILE=/etc/nx9-dns-server/keys/Kanydomain.tld.key + - DNS_FORWARDERS=8.8.8.8:53,1.1.1.1:53 + - DNS_NS_RECORDS=ns1.anydomain.tld.,ns2.anydomain.tld. + - WEB_UI_BIND=0.0.0.0:8080 + - API_BIND=0.0.0.0:8081 + restart: unless-stopped \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6ef71c8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,57 @@ +# Build stage +FROM rust:1.72-slim-bookworm AS builder + +# Install necessary build dependencies +RUN apt-get update && apt-get install -y \ + musl-tools \ + build-essential \ + pkg-config \ + libssl-dev \ + && rm -rf /var/lib/apt/lists/* + +# Add support for cross-compilation to Alpine +RUN rustup target add x86_64-unknown-linux-musl + +# Create a new empty project +WORKDIR /app +COPY . . + +# Build the project with musl target +RUN cargo build --target x86_64-unknown-linux-musl --release + +# Runtime stage +FROM alpine:3.18 + +# Install runtime dependencies +RUN apk --no-cache add ca-certificates sqlite tzdata + +# Create a non-root user for running the application +RUN addgroup -S dns && adduser -S dnsuser -G dns + +# Create necessary directories +RUN mkdir -p /var/nx9-dns-server /var/log/nx9-dns-server /etc/nx9-dns-server +RUN chown -R dnsuser:dns /var/nx9-dns-server /var/log/nx9-dns-server /etc/nx9-dns-server + +# Copy the compiled binary +COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/dns_server /usr/local/bin/ +RUN chmod +x /usr/local/bin/dns_server + +# Copy configuration files +COPY --from=builder /app/conf/dns_records.sql /etc/nx9-dns-server/ +COPY --from=builder /app/conf/dns.db.sample /etc/nx9-dns-server/ + +# Expose DNS ports +EXPOSE 53/udp 53/tcp +# Expose Web UI port +EXPOSE 8080/tcp +# Expose API port +EXPOSE 8081/tcp + +# Set working directory +WORKDIR /var/nx9-dns-server + +# Switch to non-root user +USER dnsuser + +# Command to run the application +CMD ["/usr/local/bin/dns_server"] \ No newline at end of file diff --git a/Flow-Chart.png b/Flow-Chart.png new file mode 100644 index 0000000..8ef146e Binary files /dev/null and b/Flow-Chart.png differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..af43f8d --- /dev/null +++ b/README.md @@ -0,0 +1,575 @@ +# nx9-dns-server + +**nx9-dns-server** is a high-performance, RFC-compliant authoritative DNS server implemented in Rust. It is designed for any domain (e.g., `anydomain.tld`), supporting a wide range of DNS record types, DNSSEC, and robust operational features. The server is optimized for reliability, security, and ease of deployment in production environments. + +--- + +## Table of Contents + +- [Features](#features) +- [Architecture](#architecture) +- [DNS Record Management](#dns-record-management) +- [Web UI](#web-ui) +- [API Service](#api-service) +- [User Management](#user-management) +- [DNSSEC Support](#dnssec-support) +- [How to Create DNSSEC_KEY_FILE](#how-to-create-dnssec_key_file) +- [Deployment](#deployment) + - [Traditional Deployment](#traditional-deployment) + - [Docker Deployment](#docker-deployment) +- [Configuration](#configuration) +- [Testing & Diagnostics](#testing--diagnostics) +- [Roadmap](#roadmap) +- [Contributing](#contributing) +- [License](#license) +- [Acknowledgements](#acknowledgements) + +--- + +## Features + +- **Authoritative DNS**: Serves authoritative responses for all queries to your domain (e.g., `anydomain.tld`). +- **Multi-Record Support**: Handles A, AAAA, MX, NS, SOA, PTR, TXT, and CNAME records. +- **DNSSEC Ready**: Supports DNSSEC key management and secure record signing. +- **High Performance**: Asynchronous networking (UDP/TCP) via Tokio for handling thousands of concurrent queries. +- **RFC Compliance**: Strict adherence to DNS protocol standards for interoperability. +- **Extensible Storage**: Uses SQLite for DNS record storage, allowing easy updates and migrations. +- **Easy Deployment**: Includes deployment and update scripts for smooth operational workflows. +- **Comprehensive Logging**: Integrates with `env_logger` for detailed runtime diagnostics. +- **Web Interface**: (Coming soon) Administrative web UI for DNS record management. +- **API Service**: (Coming soon) RESTful API service for programmatic DNS record management. +- **User Management**: (Coming soon) Multi-user access control with role-based permissions. + +--- + +## Architecture + +- **Language**: Rust (2021 edition) +- **Async Runtime**: [Tokio](https://tokio.rs/) +- **Database**: SQLite via [rusqlite](https://crates.io/crates/rusqlite) +- **Logging**: [log](https://crates.io/crates/log) and [env_logger](https://crates.io/crates/env_logger) +- **Error Handling**: [thiserror](https://crates.io/crates/thiserror) +- **DNSSEC**: Built-in support for key loading and RRSIG/DS/DNSKEY records +- **Web Framework**: (Coming soon) [Rocket](https://rocket.rs/) or [Axum](https://github.com/tokio-rs/axum) for UI and API endpoints +- **Authentication**: (Coming soon) JWT-based authentication and role-based authorization +- **Containerization**: Docker support with Alpine Linux for minimal footprint +- **Cross-Compilation**: Support for building from Debian to Alpine Linux (musl) target + +--- + +## DNS Record Management + +DNS records are managed in an SQLite database (`dns.db`). The schema supports multiple records per domain and type, and can be easily updated using SQL scripts. + +**Example schema (`dns_records.sql`):** +```sql +CREATE TABLE IF NOT EXISTS dns_records ( + domain TEXT NOT NULL, + record_type TEXT NOT NULL, + value TEXT NOT NULL, + ttl INTEGER DEFAULT 3600, + PRIMARY KEY (domain, record_type, value) +) WITHOUT ROWID; +``` + +**Sample records:** +```sql +INSERT OR REPLACE INTO dns_records VALUES +('anydomain.tld', 'A', '203.0.113.10', 3600), +('anydomain.tld', 'MX', '10 mail.anydomain.tld', 3600), +('anydomain.tld', 'NS', 'ns1.anydomain.tld', 3600), +('anydomain.tld', 'NS', 'ns2.anydomain.tld', 3600), +('anydomain.tld', 'SOA', 'ns1.anydomain.tld hostmaster.anydomain.tld 1 10800 3600 604800 86400', 3600), +('anydomain.tld', 'TXT', '"v=spf1 a mx ~all"', 3600), +('www.anydomain.tld', 'A', '203.0.113.10', 3600); +``` + +--- + +## Web UI + +> 🚧 **Under Development - Seeking Contributors!** 🚧 +> +> We're actively looking for community contributions to our Web UI implementation. If you have experience with Rust web frameworks (Rocket/Axum) and modern frontend technologies (React/Vue/Svelte), please consider contributing! + +The planned Web UI will provide: + +- **Dashboard**: Visual overview of DNS zone statistics and recent queries +- **Record Management**: Intuitive interface for creating, viewing, updating, and deleting DNS records +- **DNSSEC Management**: UI for key generation, rotation, and signature verification +- **Audit Logging**: Visual timeline of all record changes with user attribution +- **Responsive Design**: Mobile-friendly interface for management on any device + +**Tech Stack (Proposed):** +- Backend: Rust with Rocket or Axum +- Frontend: TypeScript with React or Svelte +- Authentication: JWT-based with session management + +**Contribution Areas:** +- UI/UX design mockups +- Frontend component development +- API integration +- Automated testing +- Documentation + +If interested in contributing, please open an issue discussing your implementation approach before submitting PRs. + +--- + +## API Service + +> 🚧 **Under Development - Seeking Contributors!** 🚧 +> +> We're building a RESTful API service for programmatic DNS record management. Contributors with experience in API design and Rust web services are welcome! + +The DNS record management API will provide: + +- **Full CRUD Operations**: Create, read, update, and delete DNS records via REST endpoints +- **Batch Operations**: Support for bulk record changes in a single request +- **Validation**: Strict validation of record syntax and domain integrity +- **Rate Limiting**: Protection against API abuse +- **Authentication**: Secure token-based authentication with scoped permissions +- **Webhooks**: (Planned) Event notifications for record changes + +**Planned Endpoints:** +``` +GET /api/v1/zones # List all zones +POST /api/v1/zones # Create new zone +GET /api/v1/zones/{zone} # Get zone details +PUT /api/v1/zones/{zone} # Update zone properties +DELETE /api/v1/zones/{zone} # Remove zone + +GET /api/v1/zones/{zone}/records # List all records in zone +POST /api/v1/zones/{zone}/records # Create new record +GET /api/v1/zones/{zone}/records/{id} # Get record details +PUT /api/v1/zones/{zone}/records/{id} # Update record +DELETE /api/v1/zones/{zone}/records/{id} # Remove record + +POST /api/v1/zones/{zone}/records/batch # Batch create/update/delete +``` + +If you're interested in contributing to the API service, please refer to our API design document in the project wiki. + +--- + +## User Management + +> 🚧 **Under Development - Community Input Requested!** 🚧 +> +> We're designing a user management system and need input from the community on requirements and features. + +Planned user management features: + +- **Multi-User Support**: Multiple administrator and operator accounts +- **Role-Based Access Control**: Granular permissions for different user roles +- **Authentication Options**: Local accounts and potential OAuth/LDAP integration +- **Audit Trail**: Comprehensive logging of all user actions +- **Password Policies**: Configurable password requirements and rotation policies +- **Two-Factor Authentication**: Additional security layer for administrative access +- **API Tokens**: Management of scoped API tokens for programmatic access + +**User Roles (Proposed):** +- **Administrator**: Full system access +- **Operator**: Can manage DNS records but not system settings +- **Viewer**: Read-only access to records and statistics +- **API Client**: Programmatic access via API tokens + +**We welcome community input on:** +- Authentication mechanisms +- Additional role definitions and permission scopes +- UI/UX design for user management interfaces +- Enterprise integration requirements + +Please open an issue with the tag `user-management` to share your feedback and requirements. + +--- + +## DNSSEC Support + +- **Key Management**: DNSSEC keys are loaded from environment-configured paths. +- **Record Signing**: Supports RRSIG, DS, and DNSKEY records for secure, signed DNS responses. +- **Preprocessing**: Key files can be preprocessed using provided scripts before deployment. + +--- + +## How to Create `DNSSEC_KEY_FILE` + +To enable DNSSEC for `nx9-dns-server`, you need to generate a DNSSEC key pair and provide the public key file to the server via the `DNSSEC_KEY_FILE` environment variable. Here's how you can do it using [BIND's dnssec-keygen tool](https://bind9.readthedocs.io/en/latest/reference.html#dnssec-keygen): + +### 1. Install `dnssec-keygen` + +On most Linux systems, you can install it via the package manager: + +```bash +sudo apt-get install bind9-dnsutils # Debian/Ubuntu +# or +sudo yum install bind-utils # CentOS/RHEL +``` + +### 2. Generate DNSSEC Key Pair + +Run the following command to generate a 2048-bit RSA key for your domain (replace `anydomain.tld` with your actual domain): + +```bash +dnssec-keygen -a RSASHA256 -b 2048 -n ZONE anydomain.tld +``` + +- This will produce two files in your current directory: + - `K.+008+.key` (public key) + - `K.+008+.private` (private key) + +### 3. Set the `DNSSEC_KEY_FILE` Environment Variable + +Copy the public key file (`.key`) to your server's key directory (e.g., `/var/nx9-dns-server/`): + +```bash +cp Kanydomain.tld.+008+24550.key /var/nx9-dns-server/ +``` + +Then, set the environment variable in your deployment environment or systemd service: + +```bash +export DNSSEC_KEY_FILE="/var/nx9-dns-server/Kanydomain.tld.+008+24550.key" +``` + +Or in your systemd unit file: +``` +Environment="DNSSEC_KEY_FILE=/var/nx9-dns-server/Kanydomain.tld.+008+24550.key" +``` + +### 4. (Optional) Preprocess the Key + +If your deployment uses a preprocessing script (as referenced in your `deploy.sh`), run: + +```bash +sudo chmod +x /var/nx9-dns-server/preprocess-key.sh +sudo -u dnsuser /var/nx9-dns-server/preprocess-key.sh +``` +This may normalize the key format or permissions as required by your server. + +### 5. Restart the DNS Server + +After setting the key file, restart your DNS server to load the new key: + +```bash +sudo systemctl restart dns-server.service +``` + +### 6. Verify DNSSEC is Working + +Use the provided `dnscheck.sh` script or `dig` to verify DNSSEC records: + +```bash +bash dnscheck.sh +# or manually: +dig @localhost anydomain.tld DNSKEY +dnssec +``` + +**Note:** +- Keep your `.private` key file secure and never expose it publicly. +- Only the `.key` (public) file should be referenced by the server. +- The server will load and use the public key for signing DNS responses. + +--- + +## Deployment + +### Traditional Deployment + +Deployment is automated and robust, using the provided [`deploy.sh`](deploy.sh) script. This script handles permissions, key preprocessing, SOA updates, binary replacement, and service management. + +**Typical deployment steps:** +```bash +#!/bin/bash + +set -e + +SRC_BIN="/home/youruser/apps/your-ddns/dns_server" +DEST_DIR="/var/nx9-dns-server" +DEST_BIN="$DEST_DIR/dns_server" +PREPROCESS_SCRIPT="$DEST_DIR/preprocess-key.sh" +SOA_UPDATE_SCRIPT="$DEST_DIR/soa-update.sh" + +echo "🔐 Fixing permissions and running preprocess..." +sudo chmod +x "$PREPROCESS_SCRIPT" +sudo -u dnsuser "$PREPROCESS_SCRIPT" + +echo "🛠 Updating SOA record..." +sudo chown dnsuser:dnsuser "$SOA_UPDATE_SCRIPT" +sudo chmod +x "$SOA_UPDATE_SCRIPT" +sudo -u dnsuser "$SOA_UPDATE_SCRIPT" + +echo "📄 Verifying processed.key content..." +sudo cat "$DEST_DIR/processed.key" + +echo "🛑 Stopping DNS server..." +sudo systemctl stop dns-server.service + +echo "📦 Deploying new dns_server binary..." +sudo cp "$SRC_BIN" "$DEST_BIN" +sudo chown dnsuser:dnsuser "$DEST_DIR" + +echo "🔁 Reloading systemd and restarting service..." +sudo systemctl daemon-reload +sudo systemctl restart dns-server.service + +echo "📈 Checking service status..." +sudo systemctl status dns-server.service +``` +See [`deploy.sh`](deploy.sh) for the full deployment script. + +### Docker Deployment + +We provide a Docker-based deployment option using Alpine Linux for a minimal and secure container. + +#### Dockerfile + +```Dockerfile +# Build stage +FROM rust:1.72-slim-bookworm AS builder + +# Install necessary build dependencies +RUN apt-get update && apt-get install -y \ + musl-tools \ + build-essential \ + pkg-config \ + libssl-dev \ + && rm -rf /var/lib/apt/lists/* + +# Add support for cross-compilation to Alpine +RUN rustup target add x86_64-unknown-linux-musl + +# Create a new empty project +WORKDIR /app +COPY . . + +# Build the project with musl target +RUN cargo build --target x86_64-unknown-linux-musl --release + +# Runtime stage +FROM alpine:3.18 + +# Install runtime dependencies +RUN apk --no-cache add ca-certificates sqlite tzdata + +# Create a non-root user for running the application +RUN addgroup -S dns && adduser -S dnsuser -G dns + +# Create necessary directories +RUN mkdir -p /var/nx9-dns-server /var/log/nx9-dns-server /etc/nx9-dns-server +RUN chown -R dnsuser:dns /var/nx9-dns-server /var/log/nx9-dns-server /etc/nx9-dns-server + +# Copy the compiled binary +COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/dns_server /usr/local/bin/ +RUN chmod +x /usr/local/bin/dns_server + +# Copy configuration files +COPY --from=builder /app/conf/dns_records.sql /etc/nx9-dns-server/ +COPY --from=builder /app/conf/dns.db.sample /etc/nx9-dns-server/ + +# Expose DNS ports +EXPOSE 53/udp 53/tcp +# Expose Web UI port +EXPOSE 8080/tcp +# Expose API port +EXPOSE 8081/tcp + +# Set working directory +WORKDIR /var/nx9-dns-server + +# Switch to non-root user +USER dnsuser + +# Command to run the application +CMD ["/usr/local/bin/dns_server"] +``` + +#### Building the Docker Image + +```bash +# Clone the repository +git clone https://github.com/thakares/nx9-dns-server.git +cd nx9-dns-server + +# Build the Docker image +docker build -t nx9-dns-server:latest . +``` + +#### Running the Container + +```bash +# Run with basic configuration +docker run -d --name nx9-dns \ + -p 53:53/udp -p 53:53/tcp \ + -p 8080:8080 -p 8081:8081 \ + -v /path/to/dns.db:/var/nx9-dns-server/dns.db \ + -v /path/to/keys:/etc/nx9-dns-server/keys \ + -e DNS_BIND=0.0.0.0:53 \ + -e DNS_DB_PATH=/var/nx9-dns-server/dns.db \ + -e DNSSEC_KEY_FILE=/etc/nx9-dns-server/keys/Kanydomain.tld.key \ + -e WEB_UI_BIND=0.0.0.0:8080 \ + -e API_BIND=0.0.0.0:8081 \ + nx9-dns-server:latest +``` + +#### Using Docker Compose + +For more complex deployments, a `docker-compose.yml` file is recommended: + +```yaml +version: '3.8' + +services: + dns: + image: nx9-dns-server:latest + container_name: nx9-dns + ports: + - "53:53/udp" + - "53:53/tcp" + - "8080:8080" + - "8081:8081" + volumes: + - ./data/dns.db:/var/nx9-dns-server/dns.db + - ./keys:/etc/nx9-dns-server/keys + - ./logs:/var/log/nx9-dns-server + environment: + - DNS_BIND=0.0.0.0:53 + - DNS_DB_PATH=/var/nx9-dns-server/dns.db + - DNSSEC_KEY_FILE=/etc/nx9-dns-server/keys/Kanydomain.tld.key + - DNS_FORWARDERS=8.8.8.8:53,1.1.1.1:53 + - DNS_NS_RECORDS=ns1.anydomain.tld.,ns2.anydomain.tld. + - WEB_UI_BIND=0.0.0.0:8080 + - API_BIND=0.0.0.0:8081 + restart: unless-stopped +``` + +To run with Docker Compose: + +```bash +docker-compose up -d +``` + +--- + +## Configuration + +Configuration is environment-driven and highly flexible. + +**Key environment variables:** +- `DNS_BIND`: Bind address (default: `0.0.0.0:53`) +- `DNS_DB_PATH`: Path to the SQLite database (default: `dns.db`) +- `DNSSEC_KEY_FILE`: Path to DNSSEC key file +- `DNS_FORWARDERS`: Comma-separated list of upstream DNS resolvers +- `DNS_NS_RECORDS`: Comma-separated list of NS records +- `DNS_CACHE_TTL`: Cache TTL in seconds +- `WEB_UI_BIND`: Bind address for web interface (default: `127.0.0.1:8080`) +- `API_BIND`: Bind address for API service (default: `127.0.0.1:8081`) +- `AUTH_SECRET`: Secret key for JWT token signing +- `ADMIN_PASSWORD`: Initial admin password (only used if no users exist) + +**Example:** +```bash +export DNS_BIND="0.0.0.0:53" +export DNS_DB_PATH="/var/nx9-dns-server/dns.db" +export DNSSEC_KEY_FILE="/var/nx9-dns-server/Kanydomain.tld.+008+24550.key" +export DNS_FORWARDERS="8.8.8.8:53,1.1.1.1:53" +export DNS_NS_RECORDS="ns1.anydomain.tld.,ns2.anydomain.tld." +export WEB_UI_BIND="0.0.0.0:8080" +export API_BIND="0.0.0.0:8081" +export AUTH_SECRET="your-secure-random-string-here" +``` + +--- + +## Testing & Diagnostics + +A suite of shell scripts is provided for diagnostics and record verification: + +- **dnscheck.sh**: Runs a series of `dig` queries for all major record types and DNSSEC. +- **dns_dump.sh**: Dumps all record types for a given domain. +- **api_test.sh**: (Coming soon) Tests the API endpoints with sample requests. +- **performance_test.sh**: (Coming soon) Benchmarks server performance under load. + +**Example usage:** +```bash +bash dnscheck.sh +bash dns_dump.sh anydomain.tld +``` + +--- + +## Roadmap + +Our planned features and improvements: + +### Short-term (1-3 months) +- [x] Core DNS server functionality +- [x] DNSSEC implementation +- [ ] Web UI development (in progress) +- [ ] RESTful API service (in progress) +- [ ] User management system (planning) +- [ ] Docker container support + +### Medium-term (3-6 months) +- [ ] Clustered deployment support +- [ ] Metrics and monitoring integration (Prometheus) +- [ ] Zone transfer (AXFR/IXFR) support +- [ ] Dynamic DNS update protocol (RFC 2136) +- [ ] DNSSEC key rotation automation +- [ ] Kubernetes Helm charts for enterprise deployment + +### Long-term (6+ months) +- [ ] Secondary/slave DNS server support +- [ ] Geo-based DNS responses +- [ ] DNS over HTTPS (DoH) support +- [ ] DNS over TLS (DoT) support +- [ ] Record templating system + +--- + +## Contributing + +Contributions, bug reports, and feature requests are welcome! Please open issues or pull requests via GitHub. + +### Priority Contribution Areas +We're actively seeking contributions in these areas: + +1. **Web UI Development**: Frontend components and integration with the backend +2. **API Service**: RESTful API implementation for DNS record management +3. **User Management**: Authentication, authorization, and user interface +4. **Documentation**: Improving guides and examples +5. **Testing**: Unit tests, integration tests, and automated CI pipelines + +### How to Contribute +1. Fork the repository +2. Create a feature branch: `git checkout -b feature/amazing-feature` +3. Commit your changes: `git commit -m 'Add some amazing feature'` +4. Push to the branch: `git push origin feature/amazing-feature` +5. Open a Pull Request + +Please see [CONTRIBUTING.md](CONTRIBUTING.md) for detailed contribution guidelines. + +--- + +## License + +This project is licensed under the [GNU General Public License v3.0 (GPLv3)](LICENSE). + +--- + +## Acknowledgements + +- [Tokio](https://tokio.rs/) for async runtime +- [rusqlite](https://crates.io/crates/rusqlite) for SQLite integration +- [dig](https://linux.die.net/man/1/dig) for DNS diagnostics +- Community contributors and supporters + +--- + +**nx9-dns-server** is developed and maintained by Sunil Purushottam Thakare . +For more information, see the source code or contact the maintainer via GitHub. + +--- + +**Tip:** +Replace `anydomain.tld` with your actual domain throughout the configuration and database files. diff --git a/deploy-dns-server.sh b/deploy-dns-server.sh new file mode 100644 index 0000000..9da394a --- /dev/null +++ b/deploy-dns-server.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +set -e # Exit on error + +echo "🔧 Making preprocess-key.sh executable..." +sudo chmod +x /var/nx9-dns-server/preprocess-key.sh + +echo "👤 Running preprocess-key.sh as dnsuser..." +sudo -u dnsuser /var/nx9-dns-server/preprocess-key.sh + +echo "🔧 Setting ownership and permissions for soa-update.sh..." +sudo chown dnsuser:dnsuser /var/nx9-dns-server/soa-update.sh +sudo chmod +x /var/nx9-dns-server/soa-update.sh + +echo "👤 Running soa-update.sh as dnsuser..." +sudo -u dnsuser /var/nx9-dns-server/soa-update.sh + +echo "📄 Checking output of processed.key..." +sudo cat /var/nx9-dns-server/processed.key + +echo "🛑 Stopping dns-server.service..." +sudo systemctl stop dns-server.service + +echo "📦 Deploying compiled binary to /var/nx9-dns-server..." +sudo cp /home/sunil/apps/nx9-bzo-ddns/dns_server /var/nx9-dns-server/dns_server + +echo "👤 Fixing ownership of /var/nx9-dns-server..." +sudo chown dnsuser:dnsuser /var/nx9-dns-server + +echo "🔄 Reloading systemd daemon and restarting service..." +sudo systemctl daemon-reload +sudo systemctl restart dns-server.service + +echo "📡 Checking status of dns-server.service..." +sudo systemctl status dns-server.service diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..f6769dd --- /dev/null +++ b/deploy.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -e + +# Paths +SRC_BIN="/home//apps/nx9-dns-server/dns_server" +DEST_DIR="/var/nx9-dns-server" +DEST_BIN="$DEST_DIR/nx9-dns_server" +PREPROCESS_SCRIPT="$DEST_DIR/preprocess-key.sh" +SOA_UPDATE_SCRIPT="$DEST_DIR/soa-update.sh" + +echo "🔐 Fixing permissions and running preprocess..." +sudo chmod +x "$PREPROCESS_SCRIPT" +sudo -u dnsuser "$PREPROCESS_SCRIPT" + +echo "🛠 Updating SOA record..." +sudo chown dnsuser:dnsuser "$SOA_UPDATE_SCRIPT" +sudo chmod +x "$SOA_UPDATE_SCRIPT" +sudo -u dnsuser "$SOA_UPDATE_SCRIPT" + +echo "📄 Verifying processed.key content..." +sudo cat "$DEST_DIR/processed.key" + +echo "🛑 Stopping DNS server..." +sudo systemctl stop dns-server.service + +echo "📦 Deploying new dns_server binary..." +sudo cp "$SRC_BIN" "$DEST_BIN" +sudo chown dnsuser:dnsuser "$DEST_DIR" + +echo "🔁 Reloading systemd and restarting service..." +sudo systemctl daemon-reload +sudo systemctl restart dns-server.service + +echo "📈 Checking service status..." +sudo systemctl status dns-server.service diff --git a/dns_dump.sh b/dns_dump.sh new file mode 100644 index 0000000..0a1d293 --- /dev/null +++ b/dns_dump.sh @@ -0,0 +1,9 @@ +#!/bin/bash +DOMAIN=$1 +RECORDS="A AAAA MX TXT CNAME NS SOA PTR" + +for TYPE in $RECORDS; do + echo "== $TYPE records for $DOMAIN ==" + dig $DOMAIN $TYPE + echo +done diff --git a/dns_records.sql b/dns_records.sql new file mode 100644 index 0000000..6e1dd38 --- /dev/null +++ b/dns_records.sql @@ -0,0 +1,36 @@ +-- dns.query - SQL commands to populate DNS records +BEGIN TRANSACTION; + +-- First modify the table schema to allow multiple NS records +CREATE TABLE IF NOT EXISTS dns_records ( + domain TEXT NOT NULL, + record_type TEXT NOT NULL, + value TEXT NOT NULL, + ttl INTEGER DEFAULT 3600, + PRIMARY KEY (domain, record_type, value) +) WITHOUT ROWID; + +-- Copy existing data to new table +INSERT INTO dns_records_new SELECT * FROM dns_records; + +-- Replace the old table +DROP TABLE dns_records; +ALTER TABLE dns_records_new RENAME TO dns_records; + +-- Now insert all DNS records +INSERT OR REPLACE INTO dns_records VALUES + ('33.61.254.60.in-addr.arpa', 'PTR', 'ns1.yourdomain.tld', 3600), + ('admin.yourdomain.tld', 'A', '60.254.61.33', 3600), + ('api.yourdomain.tld', 'A', '60.254.61.33', 3600), + ('yourdomain.tld', 'A', '60.254.61.33', 3600), + ('yourdomain.tld', 'MX', '10 mail.yourdomain.tld', 3600), + ('yourdomain.tld', 'NS', 'ns1.yourdomain.tld', 3600), + ('yourdomain.tld', 'NS', 'ns2.yourdomain.tld', 3600), -- This will now work with the new schema + ('yourdomain.tld', 'SOA', 'ns1.yourdomain.tld hostmaster.yourdomain.tld 1 10800 3600 604800 86400', 3600), + ('yourdomain.tld', 'TXT', '"v=spf1 a mx ~all"', 3600), + ('ddns.yourdomain.tld', 'A', '60.254.61.33', 3600), + ('ns1.yourdomain.tld', 'A', '60.254.61.33', 3600), + ('ns2.yourdomain.tld', 'A', '60.254.61.33', 3600), + ('www.yourdomain.tld', 'A', '60.254.61.33', 3600); + +COMMIT; diff --git a/dns_server.service b/dns_server.service new file mode 100644 index 0000000..938d699 --- /dev/null +++ b/dns_server.service @@ -0,0 +1,33 @@ +[Unit] +Description=NX9 DNS Server +After=network.target + +[Service] +User=dnsuser +Group=dnsuser +WorkingDirectory=/var/nx9-dns-server +ExecStart=/var/dns-server/nx9-dns_server # Run directly, skip wrapper +Restart=always +Environment=DNS_BIND=0.0.0.0:53 +Environment=DNS_ENABLE_IPV6=1 +Environment=DNS_MAX_PACKET_SIZE=4096 +Environment=DNS_DB_PATH=/var/nx9-dns-server/dns.db +Environment=DNS_NS_RECORDS=ns1.yourdomain.tld.,ns2.yourdomain.tld. +Environment=DNS_AUTHORITATIVE=1 +Environment=DNS_CACHE_TTL=300 +Environment=RUST_LOG=info +Environment=DNS_DEFAULT_DOMAIN=yourdomain.tld +Environment=DNS_DEFAULT_IP= +Environment="DNS_RECURSIVE=1" # Enable recursive resolution +Environment="DNS_CACHE_SIZE=10000" +Environment="DNS_FORWARDERS=8.8.8.8:53,1.1.1.1:53,9.9.9.9:53" + +CapabilityBoundingSet=CAP_NET_BIND_SERVICE +AmbientCapabilities=CAP_NET_BIND_SERVICE +ReadWritePaths=/var/nx9-dns-server +ProtectSystem=full +LimitNOFILE=65536 + +[Install] +WantedBy=multi-user.target + diff --git a/dns_wrapper.sh b/dns_wrapper.sh new file mode 100644 index 0000000..07ff78e --- /dev/null +++ b/dns_wrapper.sh @@ -0,0 +1,22 @@ +# Create a file at /var/nx9-dns-server/dns_wrapper.sh +#!/bin/bash +# Log start with timestamp +echo "Starting DNS server at $(date)" >> /var/log/nx9-dns_server_wrapper.log + +# Export all environment variables explicitly +export DNS_DB_PATH=/var/nx9-dns-server/dns.db +export DNS_BIND=0.0.0.0:53 +export DNS_ENABLE_IPV6=1 +export DNS_MAX_PACKET_SIZE=4096 +export DNS_NS_RECORDS=ns1.yourdomain.tld.,ns2.yourdomain.tld. +export DNS_AUTHORITATIVE=1 +export DNS_CACHE_TTL=300 +export RUST_LOG=info +export DNS_DEFAULT_DOMAIN=yourdomain.tld +export DNS_DEFAULT_IP= + +# Print environment for debugging +env >> /var/log/nx9-dns-server_wrapper.log + +# Run the DNS server +/var/dns-server/nx9-dns-server diff --git a/dnscheck.sh b/dnscheck.sh new file mode 100644 index 0000000..e37be5b --- /dev/null +++ b/dnscheck.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +DOMAIN="yourdomain.tld" +DNS_SERVER="192.168.1.200" # your LAN Server Address + +echo "=== DNS Diagnostic Report for $DOMAIN using $DNS_SERVER ===" +echo + +# DNSKEY with DNSSEC +echo ">>> DNSKEY (with DNSSEC):" +dig @"$DNS_SERVER" "$DOMAIN." DNSKEY +dnssec +echo + +# DS record +echo ">>> DS Record:" +dig @"$DNS_SERVER" "$DOMAIN." DS +echo + +# NS record +echo ">>> NS Record:" +dig @"$DNS_SERVER" "$DOMAIN." NS +echo + +# MX record +echo ">>> MX Record:" +dig @"$DNS_SERVER" "$DOMAIN." MX +echo + +# SOA record +echo ">>> SOA Record:" +dig @"$DNS_SERVER" "$DOMAIN." SOA +echo + +# A record +echo ">>> A Record:" +dig @"$DNS_SERVER" "$DOMAIN." A +echo + +# Optional: check AAAA (IPv6) record +echo ">>> AAAA (IPv6) Record:" +dig @"$DNS_SERVER" "$DOMAIN." AAAA +echo + +echo "=== End of DNS Report ===" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d090007 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +version: '3.8' + +services: + dns: + image: nx9-dns-server:latest + container_name: nx9-dns + ports: + - "53:53/udp" + - "53:53/tcp" + - "8080:8080" + - "8081:8081" + volumes: + - ./data/dns.db:/var/nx9-dns-server/dns.db + - ./keys:/etc/nx9-dns-server/keys + - ./logs:/var/log/nx9-dns-server + environment: + - DNS_BIND=0.0.0.0:53 + - DNS_DB_PATH=/var/nx9-dns-server/dns.db + - DNSSEC_KEY_FILE=/etc/nx9-dns-server/keys/Kanydomain.tld.key + - DNS_FORWARDERS=8.8.8.8:53,1.1.1.1:53 + - DNS_NS_RECORDS=ns1.anydomain.tld.,ns2.anydomain.tld. + - WEB_UI_BIND=0.0.0.0:8080 + - API_BIND=0.0.0.0:8081 + restart: unless-stopped \ No newline at end of file diff --git a/flow-chart.png b/flow-chart.png new file mode 100644 index 0000000..8ef146e Binary files /dev/null and b/flow-chart.png differ diff --git a/preprocess-key.sh b/preprocess-key.sh new file mode 100644 index 0000000..040c696 --- /dev/null +++ b/preprocess-key.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +INPUT_FILE="/var/nx9-dns-server/Kyourdomain.+nnn+nnnnn.key" +OUTPUT_FILE="/var/nx9-dns-server/processed.key" + +# Extract and clean the DNSKEY record +DNSKEY_RECORD=$(grep -E '^[^;].*IN[[:space:]]+DNSKEY' "$INPUT_FILE" | head -1) + +if [ -z "$DNSKEY_RECORD" ]; then + echo "Error: No valid DNSKEY record found" >&2 + exit 1 +fi + +# Format: "domain. IN DNSKEY flags protocol algorithm key" +echo "$DNSKEY_RECORD" | awk '{ + # Remove comments and extra spaces + gsub(/;.*$/, ""); + gsub(/[[:space:]]+/, " "); + # Reconstruct with clean base64 + printf "%s %s %s %s %s %s ", $1, $2, $3, $4, $5, $6; + # Print key without any whitespace + for (i=7; i<=NF; i++) printf "%s", $i; + print ""; +}' > "$OUTPUT_FILE" + +exit 0 diff --git a/soa-update.sh b/soa-update.sh new file mode 100644 index 0000000..f94356f --- /dev/null +++ b/soa-update.sh @@ -0,0 +1,17 @@ +#!/bin/bash +DB_PATH="/var/nx9-dns-server/dns.db" +ZONE="yourdomain.tld" + +# Get today's date and an incremental suffix +DATE=$(date +%Y%m%d) +SERIAL_SUFFIX="01" +NEW_SERIAL="${DATE}${SERIAL_SUFFIX}" + +# Update SOA value +sqlite3 "$DB_PATH" < = OnceLock::new(); + +/// An entry in the DNS cache. +#[derive(Debug, Clone)] +pub struct CacheEntry { + /// The IP address for the domain. + pub ip: String, + + /// When this entry was added to the cache. + pub inserted: SystemTime, + + /// Time-to-live in seconds. + pub ttl: u64, +} + +/// Cache for DNS records to improve performance. +#[derive(Debug, Clone)] +pub struct DnsCache { + /// Map of domain names to cache entries. + pub entries: Arc>>, + + /// List of NS records for zones this server is authoritative for. + pub ns_records: Vec, +} + +impl DnsCache { + /// Create a new DNS cache. + /// + /// # Arguments + /// * `ns_records` - List of NS records for zones this server is authoritative for. + /// + /// # Returns + /// A new `DnsCache` instance. + pub fn new(ns_records: Vec) -> Self { + Self { + entries: Arc::new(Mutex::new(HashMap::new())), + ns_records, + } + } + + /// Get a cached IP address for a domain. + /// + /// # Arguments + /// * `domain` - The domain name to look up. + /// + /// # Returns + /// An `Option` containing the IP address and TTL if found and not expired. + pub fn get(&self, domain: &str) -> Option<(String, u64)> { + let cache = self.entries.lock().unwrap(); + if let Some(entry) = cache.get(domain) { + if entry.inserted.elapsed().map(|d| d.as_secs() <= entry.ttl).unwrap_or(true) { + return Some((entry.ip.clone(), entry.ttl)); + } + } + None + } + + /// Add or update a domain in the cache. + /// + /// # Arguments + /// * `domain` - The domain name to cache. + /// * `ip` - The IP address for the domain. + /// * `ttl` - Time-to-live in seconds. + pub fn set(&self, domain: String, ip: String, ttl: u64) { + let mut cache = self.entries.lock().unwrap(); + cache.insert( + domain, + CacheEntry { + ip, + inserted: SystemTime::now(), + ttl, + }, + ); + } + + /// Remove expired entries from the cache. + pub fn cleanup(&self) { + let mut cache = self.entries.lock().unwrap(); + cache.retain(|_, entry| { + entry.inserted.elapsed().map(|d| d.as_secs() <= entry.ttl).unwrap_or(true) + }); + debug!("Cache cleanup completed"); + } +} \ No newline at end of file diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..c358675 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,117 @@ +//! Configuration for the DNS server. +//! +//! This module defines the configuration structure and methods to load +//! configuration from environment variables. +#![allow(dead_code)] +#[allow(unused_variables)] + +use std::{env, fs, net::SocketAddr}; +use log::{error, info}; + +use crate::errors::DnsError; + +/// Default TTL for DNS records in seconds. +pub const DEFAULT_TTL: u64 = 600; + +/// Maximum size of DNS packets in bytes. +pub const MAX_PACKET_SIZE: usize = 4096; + +/// Server configuration loaded from environment variables. +#[derive(Debug, Clone)] +pub struct ServerConfig { + /// Address to bind the DNS server to. + pub bind_addr: SocketAddr, + + /// Path to the SQLite database file. + pub db_path: String, + + /// Time-to-live for cached DNS records. + pub cache_ttl: u64, + + /// Whether to enable IPv6 support. + pub enable_ipv6: bool, + + /// Maximum size of DNS packets. + pub max_packet_size: usize, + + /// Whether this server is authoritative for its zones. + pub authoritative: bool, + + /// List of NS records for zones this server is authoritative for. + pub ns_records: Vec, + + /// Default domain for the server. + pub default_domain: String, + + /// Default IP address for the server. + pub default_ip: String, + + /// List of upstream DNS servers to forward queries to. + pub forwarders: Vec, + + /// List of DS records for DNSSEC. + pub ds_records: Vec, + + /// List of DNSKEY records for DNSSEC. + pub dnskey_records: Vec, +} + +impl ServerConfig { + /// Load server configuration from environment variables. + /// + /// # Returns + /// A `Result` containing either the loaded `ServerConfig` or a `DnsError`. + pub fn from_env() -> Result { + let bind_addr = env::var("DNS_BIND") + .unwrap_or_else(|_| "0.0.0.0:53".into()) + .parse() + .map_err(|_| DnsError::Config("Invalid DNS_BIND address".into()))?; + + let forwarders = env::var("DNS_FORWARDERS") + .unwrap_or_else(|_| "8.8.8.8:53,1.1.1.1:53,9.9.9.9:53".into()) + .split(',') + .filter_map(|s| s.trim().parse().ok()) + .collect(); + + let key_path = env::var("DNSSEC_KEY_FILE").unwrap_or_else(|_| "Kbzo.in.+008+24550.key".to_string()); + let dnskey_records = match fs::read_to_string(&key_path) { + Ok(content) => { + info!("Loaded DNSSEC key from {}", key_path); + vec![content.trim().to_string()] + }, + Err(e) => { + error!("Failed to load DNSSEC key from {}: {}", key_path, e); + vec![] + } + }; + + Ok(Self { + bind_addr, + db_path: env::var("DNS_DB_PATH").unwrap_or_else(|_| "dns.db".into()), + cache_ttl: env::var("DNS_CACHE_TTL") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(DEFAULT_TTL), + enable_ipv6: env::var("DNS_ENABLE_IPV6") + .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) + .unwrap_or(false), + max_packet_size: env::var("DNS_MAX_PACKET_SIZE") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(MAX_PACKET_SIZE), + authoritative: env::var("DNS_AUTHORITATIVE") + .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) + .unwrap_or(false), + ns_records: env::var("DNS_NS_RECORDS") + .map(|v| v.split(',').map(|s| s.trim().to_string()).collect()) + .unwrap_or_else(|_| vec!["ns1.yourdomain.tld.".into(), "ns2.yourdomain.tld.".into()]), + default_domain: env::var("DNS_DEFAULT_DOMAIN").unwrap_or_else(|_| "bzo.in".into()), + default_ip: env::var("DNS_DEFAULT_IP").unwrap_or_else(|_| "".into()), + ds_records: vec![ + "yourdomain.tld. IN DS 24550 8 2 1F21CA282945434EE0662805430599CB2A6C479D9F934087150901CE2DA580A0".to_string() + ], + dnskey_records, + forwarders, + }) + } +} \ No newline at end of file diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..0acf875 --- /dev/null +++ b/src/db.rs @@ -0,0 +1,224 @@ +//! Database operations for the DNS server. +//! +//! This module provides functions for interacting with the SQLite database +//! that stores DNS records and zone information. + +use rusqlite::{params, Connection}; + +use crate::errors::DnsError; +use crate::config::ServerConfig; + +/// Information about a DNS zone. +#[derive(Debug, Clone)] +pub struct ZoneInfo { + /// The domain name of the zone. + pub name: String, + + /// List of NS records for the zone. + pub ns_records: Vec, + + /// SOA record for the zone, if available. + pub soa_record: Option, +} + +/// Initialize the DNS database. +/// +/// Creates the database schema if it doesn't exist and populates it with default records +/// if the database is empty. +/// +/// # Arguments +/// * `db_path` - Path to the SQLite database file. +/// * `default_domain` - Default domain name to use for initial records. +/// * `default_ip` - Default IP address to use for initial records. +/// +/// # Returns +/// A `Result` indicating success or failure. +pub fn init_db(db_path: &str, default_domain: &str, default_ip: &str) -> Result<(), DnsError> { + let conn = Connection::open(db_path)?; + + // Updated schema to allow multiple NS records + conn.execute( + "CREATE TABLE IF NOT EXISTS dns_records ( + domain TEXT NOT NULL, + record_type TEXT NOT NULL CHECK(record_type IN ( + 'A','AAAA','MX','TXT','NS','CNAME','PTR','SOA', + 'SRV','CAA','NAPTR','DS','DNSKEY','RRSIG','NSEC', + 'TLSA','SSHFP' + )), + value TEXT NOT NULL, + ttl INTEGER DEFAULT 3600, + PRIMARY KEY (domain, record_type, value) -- Now allows multiple NS records + ) WITHOUT ROWID", + [], + )?; + + let count: i64 = conn.query_row("SELECT COUNT(*) FROM dns_records", [], |row| row.get(0))?; + + if count == 0 && !default_ip.is_empty() { + let mail_domain = format!("mail.{}", default_domain); + let ns1 = format!("ns1.{}", default_domain); + let ns2 = format!("ns2.{}", default_domain); + let soa_record = format!("{} hostmaster.{} 1 10800 3600 604800 86400", ns1, default_domain); + + conn.execute_batch( + &format!( + r#" + INSERT OR IGNORE INTO dns_records VALUES('{0}', 'A', ?, 3600); + INSERT OR IGNORE INTO dns_records VALUES('www.{0}', 'A', ?, 3600); + INSERT OR IGNORE INTO dns_records VALUES('api.{0}', 'A', ?, 3600); + INSERT OR IGNORE INTO dns_records VALUES('mail.{0}', 'A', ?, 3600); + INSERT OR IGNORE INTO dns_records VALUES('ns1.{0}', 'A', ?, 3600); + INSERT OR IGNORE INTO dns_records VALUES('ns2.{0}', 'A', ?, 3600); + INSERT OR IGNORE INTO dns_records VALUES('{0}', 'MX', '10 {1}', 3600); + INSERT OR IGNORE INTO dns_records VALUES('{0}', 'TXT', '\"v=spf1 a mx ~all\"', 3600); + INSERT OR IGNORE INTO dns_records VALUES('{0}', 'NS', '{2}', 3600); + INSERT OR IGNORE INTO dns_records VALUES('{0}', 'NS', '{3}', 3600); + INSERT OR IGNORE INTO dns_records VALUES('{0}', 'SOA', '{4}', 3600); + "#, + default_domain, mail_domain, ns1, ns2, soa_record + ), + )?; + } + + Ok(()) +} + +/// Look up DNS records for a domain. +/// +/// # Arguments +/// * `db_path` - Path to the SQLite database file. +/// * `domain` - Domain name to look up. +/// +/// # Returns +/// A vector of tuples containing (value, ttl, record_type) for each record found. +pub fn lookup_records(db_path: &str, domain: &str) -> Vec<(String, u64, String)> { + let conn = Connection::open(db_path); + match conn { + Ok(conn) => { + match conn.prepare( + "SELECT value, ttl, record_type FROM dns_records WHERE domain = ?" + ) { + Ok(mut stmt) => { + match stmt.query_map(params![domain], |row| { + Ok(( + row.get(0).unwrap_or_default(), + row.get(1).unwrap_or_default(), + row.get(2).unwrap_or_default(), + )) + }) { + Ok(rows) => rows.filter_map(Result::ok).collect(), + Err(_) => Vec::new(), + } + } + Err(_) => Vec::new(), + } + } + Err(_) => Vec::new(), + } +} + +/// Get information about all zones for which this server is authoritative. +/// +/// # Arguments +/// * `db_path` - Path to the SQLite database file. +/// +/// # Returns +/// A vector of `ZoneInfo` structs containing information about each zone. +pub fn get_authoritative_zones(db_path: &str) -> Vec { + let mut zones = Vec::new(); + + if let Ok(conn) = Connection::open(db_path) { + // Find all domains with NS records (these are zones) + if let Ok(mut stmt) = conn.prepare( + "SELECT DISTINCT domain FROM dns_records WHERE record_type = 'NS'" + ) { + if let Ok(rows) = stmt.query_map([], |row| { + Ok(row.get::<_, String>(0)?) + }) { + for domain_result in rows { + if let Ok(domain) = domain_result { + let mut zone_info = ZoneInfo { + name: domain.clone(), + ns_records: Vec::new(), + soa_record: None, + }; + + // Get NS records for this zone + if let Ok(mut ns_stmt) = conn.prepare( + "SELECT value FROM dns_records WHERE domain = ? AND record_type = 'NS'" + ) { + if let Ok(ns_rows) = ns_stmt.query_map([&domain], |row| { + Ok(row.get::<_, String>(0)?) + }) { + zone_info.ns_records = ns_rows.filter_map(Result::ok).collect(); + } + } + + // Get SOA record if exists + if let Ok(mut soa_stmt) = conn.prepare( + "SELECT value FROM dns_records WHERE domain = ? AND record_type = 'SOA' LIMIT 1" + ) { + if let Ok(mut soa_rows) = soa_stmt.query_map([&domain], |row| { + Ok(row.get::<_, String>(0)?) + }) { + zone_info.soa_record = soa_rows.next().and_then(|r| r.ok()); + } + } + + zones.push(zone_info); + } + } + } + } + } + + // Add default zone information from config + if zones.is_empty() { + if let Ok(config) = ServerConfig::from_env() { + let default_zone = ZoneInfo { + name: config.default_domain.clone(), + ns_records: config.ns_records.clone(), + soa_record: Some(format!( + "{} hostmaster.{} 1 10800 3600 604800 86400", + config.ns_records.first().unwrap_or(&String::from("ns1.example.com.")), + config.default_domain + )), + }; + zones.push(default_zone); + } + } + + zones +} + +/// Find the closest parent zone for a given domain. +/// +/// # Arguments +/// * `domain` - Domain name to find the parent zone for. +/// * `zones` - List of zones to search in. +/// +/// # Returns +/// An `Option` containing the closest parent zone, if found. +pub fn find_closest_parent_zone(domain: &str, zones: &[ZoneInfo]) -> Option { + let domain_parts: Vec<&str> = domain.split('.').collect(); + + // Try progressively shorter parent domains + for i in 0..domain_parts.len() { + let candidate = domain_parts[i..].join("."); + + // Exact match + if let Some(zone) = zones.iter().find(|z| z.name == candidate) { + return Some(zone.clone()); + } + } + + // Check if domain is a subdomain of any zone we're authoritative for + for zone in zones { + if domain.ends_with(&format!(".{}", zone.name)) { + return Some(zone.clone()); + } + } + + // If no match found, return None - we are not authoritative for this domain + None +} \ No newline at end of file diff --git a/src/dns.rs b/src/dns.rs new file mode 100644 index 0000000..9216e58 --- /dev/null +++ b/src/dns.rs @@ -0,0 +1,1229 @@ +//! DNS protocol implementation. +//! +//! This module provides functions for building and parsing DNS messages. +#![allow(dead_code)] +#[allow(unused_variables)] + +use std::net::SocketAddr; +use std::io; +use log::info; +use tokio::net::{TcpStream, UdpSocket}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use base64::Engine; + +use crate::errors::DnsError; +use crate::config::{ServerConfig, DEFAULT_TTL}; +use crate::utils::{encode_dns_name, extract_domain, extract_query_type, has_opt_record, extract_edns_payload_size, extract_do_bit, parse_sig_time}; +use crate::db::{lookup_records, get_authoritative_zones, find_closest_parent_zone}; +use crate::cache::CACHE; + +/// Encode a RRSIG record. +/// +/// # Arguments +/// * `rrsig_record` - The RRSIG record string. +/// * `ttl` - Time-to-live in seconds. +/// +/// # Returns +/// A `Result` containing the encoded RRSIG record or an error. +pub fn encode_rrsig_rr( + rrsig_record: &str, + ttl: u64, +) -> Result, DnsError> { + // Example: "bzo.in. 3600 IN RRSIG DNSKEY 8 2 3600 20250601000000 20240501000000 24550 bzo.in. Q3N9z2n...base64..." + let parts: Vec<&str> = rrsig_record.split_whitespace().collect(); + if parts.len() < 10 { + return Err(DnsError::Config(format!( + "Malformed RRSIG record - expected at least 10 parts, got {}", + parts.len() + ))); + } + + let type_covered = match parts[4] { + "DNSKEY" => 48u16, + "DS" => 43u16, + "A" => 1u16, + "NS" => 2u16, + "SOA" => 6u16, + _ => return Err(DnsError::Config(format!("Unsupported type_covered: {}", parts[4]))), + }; + let algorithm = parts[5].parse::()?; + let labels = parts[6].parse::()?; + let orig_ttl = parts[7].parse::()?; + let sig_exp = parse_sig_time(parts[8])?; + let sig_inc = parse_sig_time(parts[9])?; + let key_tag = parts[10].parse::()?; + let signer_name = parts[11]; + let signature_b64 = parts[12..].join(""); // join in case signature is split + + // Encode signer_name as DNS name + let mut signer_name_wire = Vec::new(); + for label in signer_name.trim_end_matches('.').split('.') { + signer_name_wire.push(label.len() as u8); + signer_name_wire.extend_from_slice(label.as_bytes()); + } + signer_name_wire.push(0); // root + + let signature = base64::engine::general_purpose::STANDARD + .decode(signature_b64) + .map_err(|e| DnsError::Base64(e.to_string()))?; + + let mut rr = Vec::new(); + rr.extend_from_slice(&[0xc0, 0x0c]); // Name pointer to QNAME + rr.extend_from_slice(&[0x00, 0x2e]); // TYPE = RRSIG (46) + rr.extend_from_slice(&[0x00, 0x01]); // CLASS = IN + rr.extend_from_slice(&(ttl as u32).to_be_bytes()); + + // RDATA + let mut rdata = Vec::new(); + rdata.extend_from_slice(&type_covered.to_be_bytes()); + rdata.push(algorithm); + rdata.push(labels); + rdata.extend_from_slice(&orig_ttl.to_be_bytes()); + rdata.extend_from_slice(&sig_exp.to_be_bytes()); + rdata.extend_from_slice(&sig_inc.to_be_bytes()); + rdata.extend_from_slice(&key_tag.to_be_bytes()); + rdata.extend_from_slice(&signer_name_wire); + rdata.extend_from_slice(&signature); + + rr.extend_from_slice(&(rdata.len() as u16).to_be_bytes()); + rr.extend_from_slice(&rdata); + + Ok(rr) +} + +/// Forward a DNS query to upstream resolvers using UDP. +/// +/// # Arguments +/// * `forwarder` - The upstream resolver to forward to. +/// * `query` - The DNS query to forward. +/// +/// # Returns +/// A `Result` containing the response or an error. +pub async fn forward_request_udp(forwarder: SocketAddr, query: &[u8]) -> io::Result> { + let socket = UdpSocket::bind("0.0.0.0:0").await?; + socket.send_to(query, forwarder).await?; + + let mut buf = vec![0u8; 4096]; + let (size, _) = socket.recv_from(&mut buf).await?; + Ok(buf[..size].to_vec()) +} + +/// Forward a DNS query to upstream resolvers using TCP. +/// +/// # Arguments +/// * `forwarder` - The upstream resolver to forward to. +/// * `query` - The DNS query to forward. +/// +/// # Returns +/// A `Result` containing the response or an error. +pub async fn forward_request_tcp(forwarder: SocketAddr, query: &[u8]) -> io::Result> { + // Connect to the forwarder using TCP + let mut stream = TcpStream::connect(forwarder).await?; + + // Write the query with a 2-byte length prefix (per DNS over TCP) + let query_len = query.len() as u16; + stream.write_all(&query_len.to_be_bytes()).await?; + stream.write_all(query).await?; + + // Read the 2-byte length prefix of the response + let mut len_buf = [0u8; 2]; + stream.read_exact(&mut len_buf).await?; + let resp_len = u16::from_be_bytes(len_buf) as usize; + + // Read the response + let mut resp_buf = vec![0u8; resp_len]; + stream.read_exact(&mut resp_buf).await?; + + Ok(resp_buf) +} + +/// Forward a DNS query to upstream resolvers. +/// +/// # Arguments +/// * `query` - The DNS query to forward. +/// * `forwarders` - List of upstream resolvers to try. +/// +/// # Returns +/// An `Option` containing the response if successful. +pub async fn forward_to_resolvers(query: &[u8], forwarders: &[SocketAddr]) -> Option> { + for &forwarder in forwarders { + info!("Forwarding query to resolver: {}", forwarder); + if let Ok(resp) = forward_request_udp(forwarder, query).await { + info!("Received response from resolver: {}", forwarder); + return Some(resp); + } + } + None +} + +/// Forward a DNS query to upstream resolvers using TCP. +/// +/// # Arguments +/// * `query` - The DNS query to forward. +/// * `forwarders` - List of upstream resolvers to try. +/// +/// # Returns +/// An `Option` containing the response if successful. +pub async fn forward_to_resolvers_tcp(query: &[u8], forwarders: &[SocketAddr]) -> Option> { + for &forwarder in forwarders { + if let Ok(resp) = forward_request_tcp(forwarder, query).await { + return Some(resp); + } + } + None +} + +/// Send a DNS response over TCP. +/// +/// # Arguments +/// * `stream` - The TCP stream to send the response on. +/// * `response` - The DNS response to send. +/// +/// # Returns +/// A `Result` indicating success or failure. +pub async fn send_tcp_response(stream: &mut TcpStream, response: &[u8]) -> io::Result<()> { + stream.write_all(&(response.len() as u16).to_be_bytes()).await?; + stream.write_all(response).await +} + +/// Generate a DNS response for a query. +/// +/// # Arguments +/// * `query` - The DNS query. +/// * `domain` - The domain name from the query. +/// * `config` - The server configuration. +/// +/// # Returns +/// A `Result` containing the response or an error. +pub async fn generate_dns_response( + query: &[u8], + domain: String, + config: &ServerConfig, +) -> Result, DnsError> { + let query_type = extract_query_type(query).unwrap_or(1); + + // Handle DNSKEY queries first + if query_type == 48 { + if let Some(dnskey) = config.dnskey_records.first() { + return build_dnskey_response(query, dnskey, 3600, config); + } + return build_nxdomain_response(query, config.authoritative) + .ok_or(DnsError::Protocol("No DNSKEY record".into())); + } + + // Handle DS queries (type 43) + if query_type == 43 { + if let Some(ds) = config.ds_records.first() { + return build_ds_response(query, ds, 3600, config); + } + return build_nxdomain_response(query, config.authoritative) + .ok_or(DnsError::Protocol("NXDOMAIN".into())); + } + + // Check cache for A/AAAA queries + if query_type == 1 || query_type == 28 { + if let Some((ip, ttl)) = CACHE.get().unwrap().get(&domain) { + return build_dns_response(query, &ip, ttl, config); + } + } + + // Lookup records in database + let records = lookup_records(&config.db_path, &domain); + let requested_type = match query_type { + 1 => "A", + 2 => "NS", + 5 => "CNAME", + 6 => "SOA", + 12 => "PTR", + 15 => "MX", + 16 => "TXT", + 28 => "AAAA", + _ => "", + }; + + // Try exact match first + if let Some((value, ttl, _)) = records.iter() + .find(|(_, _, rtype)| rtype == requested_type) + .cloned() + { + return match requested_type { + "SOA" => build_soa_response(query, &value, ttl, domain, config), + "NS" => build_ns_response(query, &records, ttl, domain, config), + "MX" | "TXT" | "CNAME" | "PTR" => { + build_generic_record_response(query, &value, ttl, domain, query_type, config) + }, + "A" | "AAAA" => { + CACHE.get().unwrap().set(domain.clone(), value.clone(), ttl); + build_dns_response(query, &value, ttl, config) + }, + _ => Err(DnsError::Protocol("Unsupported record type".into())) + }; + } + + // Fallback for A/AAAA queries + if query_type == 1 || query_type == 28 { + if let Some((ip, ttl, _)) = records.iter() + .find(|(_, _, rtype)| rtype == "A") + .cloned() + { + CACHE.get().unwrap().set(domain.clone(), ip.clone(), ttl); + return build_dns_response(query, &ip, ttl, config); + } + } + + // If we're authoritative for this domain, return NXDOMAIN + let zones = get_authoritative_zones(&config.db_path); + if config.authoritative && find_closest_parent_zone(&domain, &zones).is_some() { + return build_nxdomain_response(query, true) + .ok_or(DnsError::Protocol("NXDOMAIN".into())); + } + + // Forward to upstream resolvers + if let Some(response) = forward_to_resolvers(query, &config.forwarders).await { + Ok(response) + } else if let Some(response) = forward_to_resolvers_tcp(query, &config.forwarders).await { + Ok(response) + } else { + Err(DnsError::Protocol("Failed to resolve domain".into())) + } +} + +/// Build a DNS response for an A or AAAA record. +/// +/// # Arguments +/// * `query` - The DNS query. +/// * `ip` - The IP address for the response. +/// * `ttl` - Time-to-live in seconds. +/// * `config` - The server configuration. +/// +/// # Returns +/// A `Result` containing the response or an error. +pub fn build_dns_response( + query: &[u8], + ip: &str, + ttl: u64, + config: &ServerConfig, +) -> Result, DnsError> { + let mut response = Vec::with_capacity(512); + + // Copy transaction ID and question from query + response.extend_from_slice(&query[..2]); + + // Set flags + // QR = 1 (response) + // OPCODE = 0 (standard query) + // AA = 1 if authoritative + // TC = 0 (not truncated) + // RD = copy from query + // RA = 1 (recursion available) + // Z = 0 + // RCODE = 0 (no error) + let flags1 = 0x80 | (query[2] & 0x01); // Set QR and preserve RD + let flags2 = 0x80; // Set RA + + response.extend_from_slice(&[ + if config.authoritative { flags1 | 0x04 } else { flags1 }, // Set AA if authoritative + flags2, + ]); + + // Copy QDCOUNT from query + response.extend_from_slice(&query[4..6]); + + // Set ANCOUNT to 1 + response.extend_from_slice(&[0x00, 0x01]); + + // Set NSCOUNT (2 if authoritative, else 0) + response.extend_from_slice(&[0x00, if config.authoritative { 0x02 } else { 0x00 }]); + + // Set ARCOUNT to 0 + response.extend_from_slice(&[0x00, 0x00]); + + // Copy question section from query + let qname_end = query[12..].iter().position(|&b| b == 0) + .ok_or_else(|| DnsError::Protocol("Invalid question format".into()))? + 13; + response.extend_from_slice(&query[12..qname_end + 4]); + + // Add answer section + // Name pointer to question + response.extend_from_slice(&[0xc0, 0x0c]); + + // Type A (0x0001) + response.extend_from_slice(&[0x00, 0x01]); + + // Class IN (0x0001) + response.extend_from_slice(&[0x00, 0x01]); + + // TTL + response.extend_from_slice(&(ttl as u32).to_be_bytes()); + + // Parse IP address + let octets: Vec = ip.split('.') + .filter_map(|s| s.parse::().ok()) + .collect(); + + if octets.len() != 4 { + return Err(DnsError::Protocol(format!("Invalid IPv4 address: {}", ip))); + } + + // RDLENGTH (4 for IPv4) + response.extend_from_slice(&[0x00, 0x04]); + + // RDATA (IP address) + response.extend_from_slice(&octets); + + // Add EDNS record if present in query + if has_opt_record(query) { + let opt_payload_size = extract_edns_payload_size(query).unwrap_or(4096); + let do_bit = extract_do_bit(query); + + // Add OPT record + response.extend_from_slice(&[0x00]); // Root domain + response.extend_from_slice(&[0x00, 0x29]); // TYPE OPT + response.extend_from_slice(&opt_payload_size.to_be_bytes()); // UDP payload size + response.extend_from_slice(&[0x00]); // Extended RCODE + response.extend_from_slice(&[0x00]); // EDNS version + + if do_bit { + response.extend_from_slice(&[0x80, 0x00]); // Flags with DO bit set + } else { + response.extend_from_slice(&[0x00, 0x00]); // Flags with DO bit clear + } + + response.extend_from_slice(&[0x00, 0x00]); // RDATA length + } + + Ok(response) +} + +/// Build a DNS response for a DS record. +/// +/// # Arguments +/// * `query` - The DNS query. +/// * `ds_record` - The DS record string. +/// * `ttl` - Time-to-live in seconds. +/// * `config` - The server configuration. +/// +/// # Returns +/// A `Result` containing the response or an error. +pub fn build_ds_response( + query: &[u8], + ds_record: &str, + ttl: u64, + config: &ServerConfig, +) -> Result, DnsError> { + let mut response = Vec::with_capacity(512); + + // Copy transaction ID and question from query + response.extend_from_slice(&query[..2]); + + // Set flags + let flags1 = 0x84; // QR=1, AA=1, RD=0 + let flags2 = 0x00; // RA=0, Z=0, RCODE=0 + + response.extend_from_slice(&[flags1, flags2]); + + // Copy QDCOUNT from query + response.extend_from_slice(&query[4..6]); + + // Set ANCOUNT to 1 + response.extend_from_slice(&[0x00, 0x01]); + + // Set NSCOUNT to 0 + response.extend_from_slice(&[0x00, 0x00]); + + // Set ARCOUNT to 1 if EDNS is present + let has_edns = has_opt_record(query); + response.extend_from_slice(&[0x00, if has_edns { 0x01 } else { 0x00 }]); + + // Copy question section from query + let qname_end = query[12..].iter().position(|&b| b == 0) + .ok_or_else(|| DnsError::Protocol("Invalid question format".into()))? + 13; + response.extend_from_slice(&query[12..qname_end + 4]); + + // Parse DS record + // Format: "yourdomain.tld. IN DS 24550 8 2 1F21CA282945434EE0662805430599CB2A6C479D9F934087150901CE2DA580A0" + let parts: Vec<&str> = ds_record.split_whitespace().collect(); + if parts.len() < 7 { + return Err(DnsError::Config(format!("Invalid DS record format: {}", ds_record))); + } + + let key_tag = parts[3].parse::() + .map_err(|_| DnsError::Config(format!("Invalid key tag: {}", parts[3])))?; + let algorithm = parts[4].parse::() + .map_err(|_| DnsError::Config(format!("Invalid algorithm: {}", parts[4])))?; + let digest_type = parts[5].parse::() + .map_err(|_| DnsError::Config(format!("Invalid digest type: {}", parts[5])))?; + let digest = hex::decode(parts[6]) + .map_err(|_| DnsError::Config(format!("Invalid digest: {}", parts[6])))?; + + // Add answer section + // Name pointer to question + response.extend_from_slice(&[0xc0, 0x0c]); + + // Type DS (0x002B) + response.extend_from_slice(&[0x00, 0x2B]); + + // Class IN (0x0001) + response.extend_from_slice(&[0x00, 0x01]); + + // TTL + response.extend_from_slice(&(ttl as u32).to_be_bytes()); + + // RDLENGTH + let rdlength = 4 + digest.len(); // 2 + 1 + 1 + digest.len() + response.extend_from_slice(&(rdlength as u16).to_be_bytes()); + + // RDATA + response.extend_from_slice(&key_tag.to_be_bytes()); + response.push(algorithm); + response.push(digest_type); + response.extend_from_slice(&digest); + + // Add EDNS record if present in query + if has_edns { + let opt_payload_size = extract_edns_payload_size(query).unwrap_or(4096); + let do_bit = extract_do_bit(query); + + // Add OPT record + response.extend_from_slice(&[0x00]); // Root domain + response.extend_from_slice(&[0x00, 0x29]); // TYPE OPT + response.extend_from_slice(&opt_payload_size.to_be_bytes()); // UDP payload size + response.extend_from_slice(&[0x00]); // Extended RCODE + response.extend_from_slice(&[0x00]); // EDNS version + + if do_bit { + response.extend_from_slice(&[0x80, 0x00]); // Flags with DO bit set + } else { + response.extend_from_slice(&[0x00, 0x00]); // Flags with DO bit clear + } + + response.extend_from_slice(&[0x00, 0x00]); // RDATA length + } + + Ok(response) +} + +/// Build a DNS response for a DNSKEY record. +/// +/// # Arguments +/// * `query` - The DNS query. +/// * `dnskey_record` - The DNSKEY record string. +/// * `ttl` - Time-to-live in seconds. +/// * `config` - The server configuration. +/// +/// # Returns +/// A `Result` containing the response or an error. +pub fn build_dnskey_response( + query: &[u8], + dnskey_record: &str, + ttl: u64, + config: &ServerConfig, +) -> Result, DnsError> { + let mut response = Vec::with_capacity(512); + + // Copy transaction ID and question from query + response.extend_from_slice(&query[..2]); + + // Set flags + let flags1 = 0x84; // QR=1, AA=1, RD=0 + let flags2 = 0x00; // RA=0, Z=0, RCODE=0 + + response.extend_from_slice(&[flags1, flags2]); + + // Copy QDCOUNT from query + response.extend_from_slice(&query[4..6]); + + // Set ANCOUNT to 1 + response.extend_from_slice(&[0x00, 0x01]); + + // Set NSCOUNT to 0 + response.extend_from_slice(&[0x00, 0x00]); + + // Set ARCOUNT to 1 if EDNS is present, 0 otherwise + let has_edns = has_opt_record(query); + response.extend_from_slice(&[0x00, if has_edns { 0x01 } else { 0x00 }]); + + // Copy question section from query + let qname_end = query[12..].iter().position(|&b| b == 0) + .ok_or_else(|| DnsError::Protocol("Invalid question format".into()))? + 13; + response.extend_from_slice(&query[12..qname_end + 4]); + + // Parse DNSKEY record + // Format: "yourdomain.tld. IN DNSKEY 256 3 8 AwEAAb/xrM..." + let parts: Vec<&str> = dnskey_record.split_whitespace().collect(); + if parts.len() < 7 { + return Err(DnsError::Config(format!("Invalid DNSKEY record format: {}", dnskey_record))); + } + + let flags = parts[3].parse::() + .map_err(|_| DnsError::Config(format!("Invalid flags: {}", parts[3])))?; + let protocol = parts[4].parse::() + .map_err(|_| DnsError::Config(format!("Invalid protocol: {}", parts[4])))?; + let algorithm = parts[5].parse::() + .map_err(|_| DnsError::Config(format!("Invalid algorithm: {}", parts[5])))?; + let public_key = parts[6..].join(""); + + // Decode base64 public key + let key_data = base64::engine::general_purpose::STANDARD + .decode(&public_key) + .map_err(|e| DnsError::Base64(e.to_string()))?; + + // Add answer section + // Name pointer to question + response.extend_from_slice(&[0xc0, 0x0c]); + + // Type DNSKEY (0x0030) + response.extend_from_slice(&[0x00, 0x30]); + + // Class IN (0x0001) + response.extend_from_slice(&[0x00, 0x01]); + + // TTL + response.extend_from_slice(&(ttl as u32).to_be_bytes()); + + // RDLENGTH + let rdlength = 4 + key_data.len(); // 2 + 1 + 1 + key_data.len() + response.extend_from_slice(&(rdlength as u16).to_be_bytes()); + + // RDATA + response.extend_from_slice(&flags.to_be_bytes()); + response.push(protocol); + response.push(algorithm); + response.extend_from_slice(&key_data); + + // Add EDNS record if present in query + if has_edns { + let opt_payload_size = extract_edns_payload_size(query).unwrap_or(4096); + let do_bit = extract_do_bit(query); + + // Add OPT record + response.extend_from_slice(&[0x00]); // Root domain + response.extend_from_slice(&[0x00, 0x29]); // TYPE OPT + response.extend_from_slice(&opt_payload_size.to_be_bytes()); // UDP payload size + response.extend_from_slice(&[0x00]); // Extended RCODE + response.extend_from_slice(&[0x00]); // EDNS version + + if do_bit { + response.extend_from_slice(&[0x80, 0x00]); // Flags with DO bit set + } else { + response.extend_from_slice(&[0x00, 0x00]); // Flags with DO bit clear + } + + response.extend_from_slice(&[0x00, 0x00]); // RDATA length + } + + Ok(response) +} + +/// Build a DNS response for a SOA record. +/// +/// # Arguments +/// * `query` - The DNS query. +/// * `soa_value` - The SOA record string. +/// * `ttl` - Time-to-live in seconds. +/// * `domain` - The domain name from the query. +/// * `config` - The server configuration. +/// +/// # Returns +/// A `Result` containing the response or an error. +pub fn build_soa_response( + query: &[u8], + soa_value: &str, + ttl: u64, + domain: String, + config: &ServerConfig, +) -> Result, DnsError> { + let mut response = Vec::with_capacity(512); + + // Copy transaction ID and question from query + response.extend_from_slice(&query[..2]); + + // Set flags + let flags1 = 0x84; // QR=1, AA=1, RD=0 + let flags2 = 0x00; // RA=0, Z=0, RCODE=0 + + response.extend_from_slice(&[flags1, flags2]); + + // Copy QDCOUNT from query + response.extend_from_slice(&query[4..6]); + + // Set ANCOUNT to 1 + response.extend_from_slice(&[0x00, 0x01]); + + // Set NSCOUNT to 0 + response.extend_from_slice(&[0x00, 0x00]); + + // Set ARCOUNT to 1 if EDNS is present, 0 otherwise + let has_edns = has_opt_record(query); + response.extend_from_slice(&[0x00, if has_edns { 0x01 } else { 0x00 }]); + + // Copy question section from query + let qname_end = query[12..].iter().position(|&b| b == 0) + .ok_or_else(|| DnsError::Protocol("Invalid question format".into()))? + 13; + response.extend_from_slice(&query[12..qname_end + 4]); + + // Parse SOA record + // Format: "ns1.example.com. hostmaster.example.com. 1 10800 3600 604800 86400" + let parts: Vec<&str> = soa_value.split_whitespace().collect(); + if parts.len() < 7 { + return Err(DnsError::Config(format!("Invalid SOA record format: {}", soa_value))); + } + + let mname = parts[0]; // Primary nameserver + let rname = parts[1]; // Hostmaster email + let serial = parts[2].parse::() + .map_err(|_| DnsError::Config(format!("Invalid serial: {}", parts[2])))?; + let refresh = parts[3].parse::() + .map_err(|_| DnsError::Config(format!("Invalid refresh: {}", parts[3])))?; + let retry = parts[4].parse::() + .map_err(|_| DnsError::Config(format!("Invalid retry: {}", parts[4])))?; + let expire = parts[5].parse::() + .map_err(|_| DnsError::Config(format!("Invalid expire: {}", parts[5])))?; + let minimum = parts[6].parse::() + .map_err(|_| DnsError::Config(format!("Invalid minimum: {}", parts[6])))?; + + // Add answer section + // Name pointer to question + response.extend_from_slice(&[0xc0, 0x0c]); + + // Type SOA (0x0006) + response.extend_from_slice(&[0x00, 0x06]); + + // Class IN (0x0001) + response.extend_from_slice(&[0x00, 0x01]); + + // TTL + response.extend_from_slice(&(ttl as u32).to_be_bytes()); + + // Encode MNAME and RNAME + let mname_wire = encode_dns_name(mname); + let rname_wire = encode_dns_name(rname); + + // RDLENGTH + let rdlength = mname_wire.len() + rname_wire.len() + 20; // 5 32-bit integers + response.extend_from_slice(&(rdlength as u16).to_be_bytes()); + + // RDATA + response.extend_from_slice(&mname_wire); + response.extend_from_slice(&rname_wire); + response.extend_from_slice(&serial.to_be_bytes()); + response.extend_from_slice(&refresh.to_be_bytes()); + response.extend_from_slice(&retry.to_be_bytes()); + response.extend_from_slice(&expire.to_be_bytes()); + response.extend_from_slice(&minimum.to_be_bytes()); + + // Add EDNS record if present in query + if has_edns { + let opt_payload_size = extract_edns_payload_size(query).unwrap_or(4096); + let do_bit = extract_do_bit(query); + + // Add OPT record + response.extend_from_slice(&[0x00]); // Root domain + response.extend_from_slice(&[0x00, 0x29]); // TYPE OPT + response.extend_from_slice(&opt_payload_size.to_be_bytes()); // UDP payload size + response.extend_from_slice(&[0x00]); // Extended RCODE + response.extend_from_slice(&[0x00]); // EDNS version + + if do_bit { + response.extend_from_slice(&[0x80, 0x00]); // Flags with DO bit set + } else { + response.extend_from_slice(&[0x00, 0x00]); // Flags with DO bit clear + } + + response.extend_from_slice(&[0x00, 0x00]); // RDATA length + } + + Ok(response) +} + +/// Build a DNS response for NS records. +/// +/// # Arguments +/// * `query` - The DNS query. +/// * `records` - The DNS records for the domain. +/// * `ttl` - Time-to-live in seconds. +/// * `domain` - The domain name from the query. +/// * `config` - The server configuration. +/// +/// # Returns +/// A `Result` containing the response or an error. +pub fn build_ns_response( + query: &[u8], + records: &[(String, u64, String)], + ttl: u64, + domain: String, + config: &ServerConfig, +) -> Result, DnsError> { + let mut response = Vec::with_capacity(512); + + // Copy transaction ID and question from query + response.extend_from_slice(&query[..2]); + + // Set flags + let flags1 = 0x84; // QR=1, AA=1, RD=0 + let flags2 = 0x00; // RA=0, Z=0, RCODE=0 + + response.extend_from_slice(&[flags1, flags2]); + + // Copy QDCOUNT from query + response.extend_from_slice(&query[4..6]); + + // Count NS records + let ns_records: Vec<&(String, u64, String)> = records.iter() + .filter(|(_, _, rtype)| rtype == "NS") + .collect(); + + // Set ANCOUNT to number of NS records + response.extend_from_slice(&(ns_records.len() as u16).to_be_bytes()); + + // Set NSCOUNT to 0 + response.extend_from_slice(&[0x00, 0x00]); + + // Set ARCOUNT to 1 if EDNS is present, 0 otherwise + let has_edns = has_opt_record(query); + response.extend_from_slice(&[0x00, if has_edns { 0x01 } else { 0x00 }]); + + // Copy question section from query + let qname_end = query[12..].iter().position(|&b| b == 0) + .ok_or_else(|| DnsError::Protocol("Invalid question format".into()))? + 13; + response.extend_from_slice(&query[12..qname_end + 4]); + + // Add answer section for each NS record + for (ns_value, ns_ttl, _) in ns_records { + // Name pointer to question + response.extend_from_slice(&[0xc0, 0x0c]); + + // Type NS (0x0002) + response.extend_from_slice(&[0x00, 0x02]); + + // Class IN (0x0001) + response.extend_from_slice(&[0x00, 0x01]); + + // TTL + response.extend_from_slice(&(*ns_ttl as u32).to_be_bytes()); + + // Encode NS name + let ns_data = encode_dns_name(ns_value); + + // RDLENGTH + response.extend_from_slice(&(ns_data.len() as u16).to_be_bytes()); + + // RDATA (NS name) + response.extend_from_slice(&ns_data); + } + + // Add EDNS record if present in query + if has_edns { + let opt_payload_size = extract_edns_payload_size(query).unwrap_or(4096); + let do_bit = extract_do_bit(query); + + // Add OPT record + response.extend_from_slice(&[0x00]); // Root domain + response.extend_from_slice(&[0x00, 0x29]); // TYPE OPT + response.extend_from_slice(&opt_payload_size.to_be_bytes()); // UDP payload size + response.extend_from_slice(&[0x00]); // Extended RCODE + response.extend_from_slice(&[0x00]); // EDNS version + + if do_bit { + response.extend_from_slice(&[0x80, 0x00]); // Flags with DO bit set + } else { + response.extend_from_slice(&[0x00, 0x00]); // Flags with DO bit clear + } + + response.extend_from_slice(&[0x00, 0x00]); // RDATA length + } + + Ok(response) +} + +/// Build a DNS response for generic record types (MX, TXT, CNAME, PTR). +/// +/// # Arguments +/// * `query` - The DNS query. +/// * `value` - The record value. +/// * `ttl` - Time-to-live in seconds. +/// * `domain` - The domain name from the query. +/// * `query_type` - The query type. +/// * `config` - The server configuration. +/// +/// # Returns +/// A `Result` containing the response or an error. +pub fn build_generic_record_response( + query: &[u8], + value: &str, + ttl: u64, + domain: String, + query_type: u16, + config: &ServerConfig, +) -> Result, DnsError> { + let mut response = Vec::with_capacity(512); + + // Copy transaction ID and question from query + response.extend_from_slice(&query[..2]); + + // Set flags + let flags1 = 0x84; // QR=1, AA=1, RD=0 + let flags2 = 0x00; // RA=0, Z=0, RCODE=0 + + response.extend_from_slice(&[flags1, flags2]); + + // Copy QDCOUNT from query + response.extend_from_slice(&query[4..6]); + + // Set ANCOUNT to 1 + response.extend_from_slice(&[0x00, 0x01]); + + // Set NSCOUNT to 0 + response.extend_from_slice(&[0x00, 0x00]); + + // Set ARCOUNT to 1 if EDNS is present, 0 otherwise + let has_edns = has_opt_record(query); + response.extend_from_slice(&[0x00, if has_edns { 0x01 } else { 0x00 }]); + + // Copy question section from query + let qname_end = query[12..].iter().position(|&b| b == 0) + .ok_or_else(|| DnsError::Protocol("Invalid question format".into()))? + 13; + response.extend_from_slice(&query[12..qname_end + 4]); + + // Add answer section + // Name pointer to question + response.extend_from_slice(&[0xc0, 0x0c]); + + // Type + response.extend_from_slice(&query_type.to_be_bytes()); + + // Class IN (0x0001) + response.extend_from_slice(&[0x00, 0x01]); + + // TTL + response.extend_from_slice(&(ttl as u32).to_be_bytes()); + + // RDATA depends on record type + match query_type { + // MX record + 15 => { + // Parse MX record: "10 mail.example.com." + let parts: Vec<&str> = value.split_whitespace().collect(); + if parts.len() < 2 { + return Err(DnsError::Config(format!("Invalid MX record format: {}", value))); + } + + let preference = parts[0].parse::() + .map_err(|_| DnsError::Config(format!("Invalid MX preference: {}", parts[0])))?; + let exchange = parts[1]; + + // Encode exchange name + let exchange_wire = encode_dns_name(exchange); + + // RDLENGTH + let rdlength = 2 + exchange_wire.len(); // preference + exchange + response.extend_from_slice(&(rdlength as u16).to_be_bytes()); + + // RDATA + response.extend_from_slice(&preference.to_be_bytes()); + response.extend_from_slice(&exchange_wire); + }, + + // TXT record + 16 => { + // Remove quotes if present + let txt_value = value.trim_matches('"'); + + // RDLENGTH + let rdlength = txt_value.len() + 1; // length byte + text + response.extend_from_slice(&(rdlength as u16).to_be_bytes()); + + // RDATA + response.push(txt_value.len() as u8); + response.extend_from_slice(txt_value.as_bytes()); + }, + + // CNAME or PTR record + 5 | 12 => { + // Encode target name + let target_wire = encode_dns_name(value); + + // RDLENGTH + response.extend_from_slice(&(target_wire.len() as u16).to_be_bytes()); + + // RDATA + response.extend_from_slice(&target_wire); + }, + + _ => return Err(DnsError::Protocol(format!("Unsupported record type: {}", query_type))), + } + + // Add EDNS record if present in query + if has_edns { + let opt_payload_size = extract_edns_payload_size(query).unwrap_or(4096); + let do_bit = extract_do_bit(query); + + // Add OPT record + response.extend_from_slice(&[0x00]); // Root domain + response.extend_from_slice(&[0x00, 0x29]); // TYPE OPT + response.extend_from_slice(&opt_payload_size.to_be_bytes()); // UDP payload size + response.extend_from_slice(&[0x00]); // Extended RCODE + response.extend_from_slice(&[0x00]); // EDNS version + + if do_bit { + response.extend_from_slice(&[0x80, 0x00]); // Flags with DO bit set + } else { + response.extend_from_slice(&[0x00, 0x00]); // Flags with DO bit clear + } + + response.extend_from_slice(&[0x00, 0x00]); // RDATA length + } + + Ok(response) +} + +/// Build a DNS response for a "not implemented" error. +/// +/// # Arguments +/// * `query` - The DNS query. +/// * `authoritative` - Whether this server is authoritative for the domain. +/// +/// # Returns +/// An `Option` containing the response if successful. +pub fn build_not_implemented_response(query: &[u8], authoritative: bool) -> Option> { + if query.len() < 12 { + return None; + } + + let mut resp = Vec::with_capacity(512); + + // Copy transaction ID from query + resp.extend_from_slice(&query[0..2]); + + // Set flags + // QR = 1 (response) + // OPCODE = copy from query + // AA = 0 or 1 depending on authoritative + // TC = 0 (not truncated) + // RD = copy from query + // RA = 1 (recursion available) + // Z = 0 + // RCODE = 4 (not implemented) + let opcode = query[2] & 0x78; // Extract OPCODE + let rd = query[2] & 0x01; // Extract RD + let flags1 = 0x80 | opcode | rd; // QR=1, OPCODE=opcode, RD=rd + let flags2 = 0x84; // RA=1, RCODE=4 (not implemented) + + resp.extend_from_slice(&[ + if authoritative { flags1 | 0x04 } else { flags1 }, // Set AA if authoritative + flags2, + ]); + + // Copy QDCOUNT from query + resp.extend_from_slice(&query[4..6]); + + // Set ANCOUNT, NSCOUNT, ARCOUNT to 0 + resp.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); + + // Copy question section from query + let mut pos = 12; + loop { + if pos >= query.len() { + return None; + } + let len = query[pos] as usize; + if len == 0 { + // End of domain name + pos += 1; + break; + } + pos += len + 1; + } + + // Include QTYPE and QCLASS (4 bytes) + if pos + 4 > query.len() { + return None; + } + pos += 4; + + // Copy question section + resp.extend_from_slice(&query[12..pos]); + + // Check for EDNS + let has_edns = has_opt_record(query); + if has_edns { + let opt_payload_size = extract_edns_payload_size(query).unwrap_or(4096); + let do_bit = extract_do_bit(query); + + // Add OPT record + resp.extend_from_slice(&[0x00]); // Root domain + resp.extend_from_slice(&[0x00, 0x29]); // TYPE OPT + resp.extend_from_slice(&opt_payload_size.to_be_bytes()); // UDP payload size + resp.extend_from_slice(&[0x00]); // Extended RCODE + resp.extend_from_slice(&[0x00]); // EDNS version + + if do_bit { + resp.extend_from_slice(&[0x80, 0x00]); // Flags with DO bit set + } else { + resp.extend_from_slice(&[0x00, 0x00]); // Flags with DO bit clear + } + + resp.extend_from_slice(&[0x00, 0x00]); // RDATA length + } + + Some(resp) +} + +/// Build a DNS response for a "name error" (NXDOMAIN). +/// +/// # Arguments +/// * `query` - The DNS query. +/// * `authoritative` - Whether this server is authoritative for the domain. +/// +/// # Returns +/// An `Option` containing the response if successful. +pub fn build_nxdomain_response(query: &[u8], authoritative: bool) -> Option> { + let mut resp = Vec::with_capacity(512); + resp.extend_from_slice(&query[0..2]); // Transaction ID + + // Extract domain from query for authority section reference + let domain = match extract_domain(query) { + Some(d) => d, + None => return None, + }; + + // Get zones we're authoritative for + let config = match ServerConfig::from_env() { + Ok(c) => c, + Err(_) => return None, + }; + + // Get authoritative zones + let zones = get_authoritative_zones(&config.db_path); + let zone = find_closest_parent_zone(&domain, &zones); + + // Set flags + // QR = 1 (response) + // OPCODE = 0 (standard query) + // AA = 1 if authoritative + // TC = 0 (not truncated) + // RD = copy from query + // RA = 1 (recursion available) + // Z = 0 + // RCODE = 3 (name error) + let rd = query[2] & 0x01; // Extract RD + let flags1 = 0x80 | rd; // QR=1, RD=rd + let flags2 = 0x83; // RA=1, RCODE=3 (name error) + + resp.extend_from_slice(&[ + if authoritative { flags1 | 0x04 } else { flags1 }, // Set AA if authoritative + flags2, + ]); + + // Copy QDCOUNT from query + resp.extend_from_slice(&query[4..6]); + + // Set ANCOUNT to 0 + resp.extend_from_slice(&[0x00, 0x00]); + + // Set NSCOUNT to 1 if we have a zone, 0 otherwise + resp.extend_from_slice(&[0x00, if zone.is_some() { 0x01 } else { 0x00 }]); + + // Check for EDNS + let has_edns = has_opt_record(query); + + // Set ARCOUNT to 1 if EDNS, 0 otherwise + resp.extend_from_slice(&[0x00, if has_edns { 0x01 } else { 0x00 }]); + + // Copy question section from query + let qname_end = match query[12..].iter().position(|&b| b == 0) { + Some(pos) => pos + 13, + None => return None, + }; + resp.extend_from_slice(&query[12..qname_end + 4]); + + // Add authority section if we have a zone + if let Some(zone) = zone { + // Add SOA record + let zone_name = encode_dns_name(&zone.name); + resp.extend_from_slice(&zone_name); + + // Type SOA (0x0006) + resp.extend_from_slice(&[0x00, 0x06]); + + // Class IN (0x0001) + resp.extend_from_slice(&[0x00, 0x01]); + + // TTL + resp.extend_from_slice(&(DEFAULT_TTL as u32).to_be_bytes()); + + // RDATA + if let Some(soa) = zone.soa_record { + // Parse SOA record + let parts: Vec<&str> = soa.split_whitespace().collect(); + if parts.len() >= 7 { + let mname = parts[0]; + let rname = parts[1]; + + // Encode MNAME and RNAME + let mname_wire = encode_dns_name(mname); + let rname_wire = encode_dns_name(rname); + + // RDLENGTH + let rdlength = mname_wire.len() + rname_wire.len() + 20; // 5 32-bit integers + resp.extend_from_slice(&(rdlength as u16).to_be_bytes()); + + // RDATA + resp.extend_from_slice(&mname_wire); + resp.extend_from_slice(&rname_wire); + + // SOA integers with reasonable defaults + resp.extend_from_slice(&1u32.to_be_bytes()); // SERIAL + resp.extend_from_slice(&10800u32.to_be_bytes()); // REFRESH + resp.extend_from_slice(&3600u32.to_be_bytes()); // RETRY + resp.extend_from_slice(&604800u32.to_be_bytes()); // EXPIRE + resp.extend_from_slice(&86400u32.to_be_bytes()); // MINIMUM + } + } + + // Add NS records + for ns in &zone.ns_records { + // Name of the zone + resp.extend_from_slice(&zone_name); + + // Type NS (0x0002) + resp.extend_from_slice(&[0x00, 0x02]); + + // Class IN (0x0001) + resp.extend_from_slice(&[0x00, 0x01]); + + // TTL + resp.extend_from_slice(&(DEFAULT_TTL as u32).to_be_bytes()); + + // RDLENGTH and RDATA (NS name) + let ns_data = encode_dns_name(ns); + resp.extend_from_slice(&(ns_data.len() as u16).to_be_bytes()); + resp.extend_from_slice(&ns_data); + } + } + + // Handle EDNS in NXDOMAIN response + if has_edns { + let opt_payload_size = extract_edns_payload_size(query).unwrap_or(4096); + + // Copy DO bit if present in request + let do_bit = extract_do_bit(query); + + // Add OPT record for EDNS + resp.extend_from_slice(&[0x00]); // Root domain + resp.extend_from_slice(&[0x00, 0x29]); // TYPE OPT + resp.extend_from_slice(&opt_payload_size.to_be_bytes()); // UDP payload size from request + resp.extend_from_slice(&[0x00]); // Extended RCODE + resp.extend_from_slice(&[0x00]); // EDNS version + + if do_bit { + resp.extend_from_slice(&[0x80, 0x00]); // Flags with DO bit set + } else { + resp.extend_from_slice(&[0x00, 0x00]); // Flags with DO bit clear + } + + resp.extend_from_slice(&[0x00, 0x00]); // RDATA length + } + + Some(resp) +} \ No newline at end of file diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..819404d --- /dev/null +++ b/src/error.rs @@ -0,0 +1,39 @@ +//! Error types for the DNS server +#![allow(dead_code)] +#[allow(unused_variables)] + +use std::io; +use rusqlite; +use thiserror::Error; + +/// Errors that can occur in the DNS server +#[derive(Debug, Error)] +pub enum DnsError { + /// I/O errors from the underlying system + #[error("I/O error: {0}")] + Io(#[from] io::Error), + + /// Database errors from SQLite operations + #[error("Database error: {0}")] + Db(#[from] rusqlite::Error), + + /// Protocol errors related to DNS message format or content + #[error("Protocol error: {0}")] + Protocol(String), + + /// Configuration errors from invalid settings + #[error("Configuration error: {0}")] + Config(String), + + /// Parse errors from string to number conversions + #[error("Parse error: {0}")] + Parse(#[from] std::num::ParseIntError), + + /// Base64 decoding errors + #[error("Base64 error: {0}")] + Base64(String), + + /// Shutdown signal received + #[error("Shutdown signal received")] + Shutdown, +} \ No newline at end of file diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..73ecc2d --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,39 @@ +//! Error types for the DNS server. +//! +//! This module defines the error types used throughout the DNS server implementation. +#![allow(dead_code)] +#[allow(unused_variables)] + +use thiserror::Error; + +/// Represents errors that can occur in the DNS server. +#[derive(Error, Debug)] +pub enum DnsError { + /// I/O errors from the standard library. + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + + /// Database errors from rusqlite. + #[error("Database error: {0}")] + Db(#[from] rusqlite::Error), + + /// Errors related to DNS protocol parsing or formatting. + #[error("Invalid DNS packet: {0}")] + Protocol(String), + + /// Configuration errors. + #[error("Configuration error: {0}")] + Config(String), + + /// Integer parsing errors. + #[error("Parse error: {0}")] + Parse(#[from] std::num::ParseIntError), + + /// Base64 decoding errors. + #[error("Base64 error: {0}")] + Base64(String), + + /// Shutdown signal received. + #[error("Shutdown signal received")] + Shutdown, +} \ No newline at end of file diff --git a/src/handlers.rs b/src/handlers.rs new file mode 100644 index 0000000..99c5c86 --- /dev/null +++ b/src/handlers.rs @@ -0,0 +1,190 @@ +//! Request handlers for the DNS server. +//! +//! This module provides functions for handling DNS requests over UDP and TCP. +#![allow(dead_code)] +#[allow(unused_variables)] + +use std::net::SocketAddr; +use std::sync::Arc; +use log::{debug, error, info, warn}; +use tokio::{ + io::AsyncReadExt, + net::{TcpListener, TcpStream, UdpSocket}, + task, +}; + +use crate::errors::DnsError; +use crate::config::ServerConfig; +use crate::utils::extract_domain; +use crate::dns::{ + build_not_implemented_response, build_nxdomain_response, generate_dns_response, + send_tcp_response, +}; + +/// Run the UDP DNS server. +/// +/// # Arguments +/// * `config` - The server configuration. +/// +/// # Returns +/// A `Result` indicating success or failure. +pub async fn run_udp_server(config: ServerConfig) -> Result<(), DnsError> { + let socket = UdpSocket::bind(config.bind_addr).await?; + info!("UDP DNS server listening on {}", config.bind_addr); + let socket = Arc::new(socket); + let mut buf = vec![0u8; config.max_packet_size]; + + loop { + match socket.recv_from(&mut buf).await { + Ok((amt, src)) => { + let query = buf[..amt].to_vec(); + let socket = socket.clone(); + let config = config.clone(); + task::spawn(async move { + if let Err(e) = handle_udp_query(query, src, socket, config).await { + warn!("UDP query error: {}", e); + } + }); + } + Err(e) => error!("UDP receive error: {}", e), + } + } +} + +/// Handle a UDP DNS query. +/// +/// # Arguments +/// * `query` - The DNS query. +/// * `src` - The source address of the query. +/// * `socket` - The UDP socket to send the response on. +/// * `config` - The server configuration. +/// +/// # Returns +/// A `Result` indicating success or failure. +pub async fn handle_udp_query( + query: Vec, + src: SocketAddr, + socket: Arc, + config: ServerConfig, +) -> Result<(), DnsError> { + if query.len() < 12 { + debug!("Received malformed query from {}", src); + return Ok(()); + } + + let opcode = (query[2] & 0x78) >> 3; + if opcode != 0 { + if let Some(response) = build_not_implemented_response(&query, config.authoritative) { + socket.send_to(&response, src).await?; + } + return Ok(()); + } + + let domain = match extract_domain(&query) { + Some(d) => d, + None => { + info!("Failed to extract domain from query"); + return Ok(()); + } + }; + + debug!("UDP query for {} from {}", domain, src); + info!("Processing query for domain: {}", domain); + + let response = match generate_dns_response(&query, domain.clone(), &config).await { + Ok(resp) => resp, + Err(_) => { + build_nxdomain_response(&query, config.authoritative) + .ok_or(DnsError::Protocol("NXDOMAIN".into()))? + } + }; + + socket.send_to(&response, src).await?; + Ok(()) +} + +/// Run the TCP DNS server. +/// +/// # Arguments +/// * `config` - The server configuration. +/// +/// # Returns +/// A `Result` indicating success or failure. +pub async fn run_tcp_server(config: ServerConfig) -> Result<(), DnsError> { + let listener = TcpListener::bind(config.bind_addr).await?; + info!("TCP DNS server listening on {}", config.bind_addr); + + loop { + match listener.accept().await { + Ok((stream, addr)) => { + let config = config.clone(); + task::spawn(async move { + if let Err(e) = handle_tcp_connection(stream, addr, config).await { + warn!("TCP connection error: {}", e); + } + }); + } + Err(e) => error!("TCP accept error: {}", e), + } + } +} + +/// Handle a TCP DNS connection. +/// +/// # Arguments +/// * `stream` - The TCP stream. +/// * `addr` - The client address. +/// * `config` - The server configuration. +/// +/// # Returns +/// A `Result` indicating success or failure. +pub async fn handle_tcp_connection( + mut stream: TcpStream, + addr: SocketAddr, + config: ServerConfig, +) -> Result<(), DnsError> { + // Read the 2-byte length prefix + let mut len_buf = [0u8; 2]; + stream.read_exact(&mut len_buf).await?; + let len = u16::from_be_bytes(len_buf) as usize; + + // Read the DNS query + let mut query = vec![0u8; len]; + stream.read_exact(&mut query).await?; + + if query.len() < 12 { + debug!("Received malformed TCP query from {}", addr); + return Ok(()); + } + + let opcode = (query[2] & 0x78) >> 3; + if opcode != 0 { + if let Some(response) = build_not_implemented_response(&query, config.authoritative) { + send_tcp_response(&mut stream, &response).await?; + } + return Ok(()); + } + + let domain = match extract_domain(&query) { + Some(d) => d, + None => { + info!("Failed to extract domain from TCP query"); + return Ok(()); + } + }; + + debug!("TCP query for {} from {}", domain, addr); + info!("Processing TCP query for domain: {}", domain); + + let response = match generate_dns_response(&query, domain.clone(), &config).await { + Ok(resp) => resp, + Err(_) => { + build_nxdomain_response(&query, config.authoritative) + .ok_or(DnsError::Protocol("NXDOMAIN".into()))? + } + }; + + // Send the response (local/cache answer) + send_tcp_response(&mut stream, &response).await?; + Ok(()) +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4118777 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,23 @@ +//! NX9 DNS Server Library +//! +//! This library provides functionality for a DNS server implementation. +//! It handles DNS queries over UDP and TCP, supports various record types, +//! and can forward queries to upstream DNS servers. + +#![allow(dead_code)] +#[allow(unused_variables)] + +// Define modules +pub mod errors; +pub mod config; +pub mod cache; +pub mod db; +pub mod dns; +pub mod handlers; +pub mod utils; +mod error; + +// Re-export commonly used items +pub use errors::DnsError; +pub use config::ServerConfig; +pub use cache::DnsCache; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..98346dc --- /dev/null +++ b/src/main.rs @@ -0,0 +1,69 @@ +//! NX9 DNS Server +//! +//! A DNS server implementation that supports various record types and can forward +//! queries to upstream DNS servers. +//! +//! Author: Sunil Purushottam Thakare +#![allow(dead_code)] +#[allow(unused_variables)] + +use log::info; +use tokio::{signal, task}; + +use nx9_dns_server::{ + cache::{CACHE, CACHE_CLEANUP_INTERVAL}, + config::ServerConfig, + db::init_db, + errors::DnsError, + handlers::{run_tcp_server, run_udp_server}, +}; + +#[tokio::main] +async fn main() -> Result<(), DnsError> { + // Initialize the logger + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) + .format_timestamp_micros() + .init(); + + // Load configuration from environment variables + let config = ServerConfig::from_env()?; + + // Initialize cache with NS records from config + let cache = CACHE.get_or_init(|| nx9_dns_server::cache::DnsCache::new(config.ns_records.clone())); + + // Initialize the database + init_db(&config.db_path, &config.default_domain, &config.default_ip)?; + + // Set up cache cleanup task + let cache_cleanup = task::spawn({ + let cache = cache.clone(); + async move { + let mut interval = tokio::time::interval(CACHE_CLEANUP_INTERVAL); + loop { + interval.tick().await; + cache.cleanup(); + } + } + }); + + // Set up shutdown signal handler + let shutdown_signal = async { + signal::ctrl_c().await.expect("Failed to listen for shutdown signal"); + info!("Shutdown signal received"); + }; + + // Start UDP and TCP servers + let udp_server = run_udp_server(config.clone()); + let tcp_server = run_tcp_server(config.clone()); + + // Wait for either a shutdown signal or server error + tokio::select! { + _ = shutdown_signal => { + info!("Initiating graceful shutdown..."); + cache_cleanup.abort(); + Ok(()) + }, + res = udp_server => res, + res = tcp_server => res, + } +} \ No newline at end of file diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..8d58a50 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,427 @@ +//! Utility functions for DNS operations. +//! +//! This module provides helper functions for parsing and encoding DNS data. +#![allow(dead_code)] +#[allow(unused_variables)] + +use std::str; +use chrono::{NaiveDateTime, TimeZone, Utc}; + +use crate::errors::DnsError; + +/// Extract the domain name from a DNS query packet. +/// +/// # Arguments +/// * `query` - The DNS query packet. +/// +/// # Returns +/// An `Option` containing the domain name if successfully extracted. +pub fn extract_domain(query: &[u8]) -> Option { + if query.len() < 12 { + return None; // DNS header is 12 bytes + } + + let mut pos = 12; // Start after header + let mut domain = String::new(); + + // Extract QNAME (domain) + loop { + if pos >= query.len() { + return None; + } + + let len = query[pos] as usize; + if len == 0 { + break; // End of QNAME + } + pos += 1; + + if pos + len > query.len() { + return None; // Invalid length + } + + if !domain.is_empty() { + domain.push('.'); + } + + let label = match str::from_utf8(&query[pos..pos + len]) { + Ok(l) => l, + Err(_) => return None, // Invalid UTF-8 + }; + domain.push_str(label); + pos += len; + } + + // Skip QTYPE and QCLASS (4 bytes) + pos += 4; + + // Verify we have enough data for at least QTYPE/QCLASS + if pos > query.len() { + return None; + } + + Some(domain) +} + +/// Extract the query type from a DNS query packet. +/// +/// # Arguments +/// * `query` - The DNS query packet. +/// +/// # Returns +/// An `Option` containing the query type as a u16 if successfully extracted. +pub fn extract_query_type(query: &[u8]) -> Option { + if query.len() < 12 { + return None; // DNS header is 12 bytes + } + + let mut pos = 12; // Start after header + + // Skip QNAME + loop { + if pos >= query.len() { + return None; + } + + let len = query[pos] as usize; + if len == 0 { + pos += 1; + break; // End of QNAME + } + + pos += len + 1; + } + + // Get QTYPE (2 bytes after QNAME) + if pos + 1 < query.len() { + Some(((query[pos] as u16) << 8) | query[pos + 1] as u16) + } else { + None + } +} + +/// Encode a domain name in DNS wire format. +/// +/// # Arguments +/// * `name` - The domain name to encode. +/// +/// # Returns +/// A vector of bytes containing the encoded domain name. +pub fn encode_dns_name(name: &str) -> Vec { + let mut out = Vec::new(); + for part in name.trim_end_matches('.').split('.') { + if part.len() > 63 { + continue; // Skip invalid labels + } + out.push(part.len() as u8); + out.extend_from_slice(part.as_bytes()); + } + out.push(0); // Null terminator + out +} + +/// Parse a signature time in YYYYMMDDHHMMSS format to seconds since epoch. +/// +/// # Arguments +/// * `s` - The signature time string. +/// +/// # Returns +/// A `Result` containing the parsed time as a u32 or an error. +pub fn parse_sig_time(s: &str) -> Result { + let dt = NaiveDateTime::parse_from_str(s, "%Y%m%d%H%M%S") + .map_err(|e| DnsError::Config(format!("Invalid sigtime: {e}")))?; + Ok(Utc.from_utc_datetime(&dt).timestamp() as u32) +} + +/// Check if a DNS query packet has an OPT record (EDNS). +/// +/// # Arguments +/// * `query` - The DNS query packet. +/// +/// # Returns +/// A boolean indicating whether the query has an OPT record. +pub fn has_opt_record(query: &[u8]) -> bool { + if query.len() < 12 { + return false; + } + + // Get ARCOUNT (number of additional records) + let arcount = ((query[10] as u16) << 8) | query[11] as u16; + if arcount == 0 { + return false; + } + + // Skip header + let mut pos = 12; + + // Skip question section + // First skip QNAME + loop { + if pos >= query.len() { + return false; + } + let len = query[pos] as usize; + if len == 0 { + pos += 1; + break; + } + pos += len + 1; + } + + // Skip QTYPE and QCLASS + pos += 4; + + // Skip answer and authority sections + let ancount = ((query[6] as u16) << 8) | query[7] as u16; + let nscount = ((query[8] as u16) << 8) | query[9] as u16; + + for _ in 0..(ancount + nscount) { + // Skip name + if pos >= query.len() { + return false; + } + + // Handle compression pointers + if (query[pos] & 0xC0) == 0xC0 { + pos += 2; // Skip compression pointer + } else { + // Skip labels + loop { + if pos >= query.len() { + return false; + } + let len = query[pos] as usize; + if len == 0 { + pos += 1; + break; + } + pos += len + 1; + } + } + + // Skip TYPE, CLASS, TTL, RDLENGTH, RDATA + if pos + 10 > query.len() { + return false; + } + let rdlength = ((query[pos + 8] as usize) << 8) | query[pos + 9] as usize; + pos += 10 + rdlength; + } + + // Check additional records for OPT + for _ in 0..arcount { + if pos >= query.len() { + return false; + } + + // OPT record has empty (root) name + if query[pos] == 0 { + // Check if TYPE is OPT (41) + if pos + 2 < query.len() && query[pos + 1] == 0 && query[pos + 2] == 41 { + return true; + } + } + + // Skip this record + if pos + 10 >= query.len() { break; } + let rdlength = ((query[pos + 8] as usize) << 8) | query[pos + 9] as usize; + pos += 10 + rdlength; + } + + false // No OPT record found +} + +/// Extract the EDNS payload size from a DNS query packet. +/// +/// # Arguments +/// * `query` - The DNS query packet. +/// +/// # Returns +/// An `Option` containing the EDNS payload size if found. +pub fn extract_edns_payload_size(query: &[u8]) -> Option { + if query.len() < 12 { + return None; + } + + // Get ARCOUNT (number of additional records) + let arcount = ((query[10] as u16) << 8) | query[11] as u16; + if arcount == 0 { + return None; + } + + // Skip header + let mut pos = 12; + + // Skip question section + // First skip QNAME + loop { + if pos >= query.len() { + return None; + } + let len = query[pos] as usize; + if len == 0 { + pos += 1; + break; + } + pos += len + 1; + } + + // Skip QTYPE and QCLASS + pos += 4; + + // Skip answer and authority sections + let ancount = ((query[6] as u16) << 8) | query[7] as u16; + let nscount = ((query[8] as u16) << 8) | query[9] as u16; + + for _ in 0..(ancount + nscount) { + // Skip name + if pos >= query.len() { + return None; + } + + // Handle compression pointers + if (query[pos] & 0xC0) == 0xC0 { + pos += 2; // Skip compression pointer + } else { + // Skip labels + loop { + if pos >= query.len() { + return None; + } + let len = query[pos] as usize; + if len == 0 { + pos += 1; + break; + } + pos += len + 1; + } + } + + // Skip TYPE, CLASS, TTL, RDLENGTH, RDATA + if pos + 10 > query.len() { + return None; + } + let rdlength = ((query[pos + 8] as usize) << 8) | query[pos + 9] as usize; + pos += 10 + rdlength; + } + + // Check additional records for OPT + for _ in 0..arcount { + if pos >= query.len() { + return None; + } + + // OPT record has empty (root) name + if query[pos] == 0 { + // Check if TYPE is OPT (41) + if pos + 5 < query.len() && query[pos + 1] == 0 && query[pos + 2] == 41 { + // Extract UDP payload size (CLASS field in OPT record) + return Some(((query[pos + 3] as u16) << 8) | query[pos + 4] as u16); + } + } + + // Skip this record + if pos + 10 >= query.len() { break; } + let rdlength = ((query[pos + 8] as usize) << 8) | query[pos + 9] as usize; + pos += 10 + rdlength; + } + + None // No OPT record found +} + +/// Extract the DO (DNSSEC OK) bit from a DNS query packet. +/// +/// # Arguments +/// * `query` - The DNS query packet. +/// +/// # Returns +/// A boolean indicating whether the DO bit is set. +pub fn extract_do_bit(query: &[u8]) -> bool { + if query.len() < 12 { + return false; + } + + // Get ARCOUNT (number of additional records) + let arcount = ((query[10] as u16) << 8) | query[11] as u16; + if arcount == 0 { + return false; + } + + // Skip header + let mut pos = 12; + + // Skip question section + // First skip QNAME + loop { + if pos >= query.len() { + return false; + } + let len = query[pos] as usize; + if len == 0 { + pos += 1; + break; + } + pos += len + 1; + } + + // Skip QTYPE and QCLASS + pos += 4; + + // Skip answer and authority sections + let ancount = ((query[6] as u16) << 8) | query[7] as u16; + let nscount = ((query[8] as u16) << 8) | query[9] as u16; + + for _ in 0..(ancount + nscount) { + // Skip name + if pos >= query.len() { + return false; + } + + // Handle compression pointers + if (query[pos] & 0xC0) == 0xC0 { + pos += 2; // Skip compression pointer + } else { + // Skip labels + loop { + if pos >= query.len() { + return false; + } + let len = query[pos] as usize; + if len == 0 { + pos += 1; + break; + } + pos += len + 1; + } + } + + // Skip TYPE, CLASS, TTL, RDLENGTH, RDATA + if pos + 10 > query.len() { + return false; + } + let rdlength = ((query[pos + 8] as usize) << 8) | query[pos + 9] as usize; + pos += 10 + rdlength; + } + + // Check additional records for OPT + for _ in 0..arcount { + if pos >= query.len() { + return false; + } + + // OPT record has empty (root) name + if query[pos] == 0 { + // Check if TYPE is OPT (41) + if pos + 7 < query.len() && query[pos + 1] == 0 && query[pos + 2] == 41 { + // Check DO bit (bit 15 of TTL field, which is used for flags in OPT) + return (query[pos + 6] & 0x80) != 0; + } + } + + // Skip this record + if pos + 10 >= query.len() { break; } + let rdlength = ((query[pos + 8] as usize) << 8) | query[pos + 9] as usize; + pos += 10 + rdlength; + } + + false // No OPT record found or no DO bit set +} \ No newline at end of file diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..416a672 --- /dev/null +++ b/start.sh @@ -0,0 +1,13 @@ +#!/bin/bash +export DNS_BIND=0.0.0.0:53 +export DNS_ENABLE_IPV6=1 +export DNS_MAX_PACKET_SIZE=4096 +export DNS_DB_PATH=/var/nx9-dns-server/dns.db +export DNS_NS_RECORDS=ns1.yourdomain.tld.,ns2.yourdomain.tld. +export DNS_AUTHORITATIVE=1 +export DNS_CACHE_TTL=300 +export RUST_LOG=info +export DNS_DEFAULT_DOMAIN=yourdomain.tld +export DNS_DEFAULT_IP= + +exec /var/dns-server/nx9-dns_server