Refactor: major codebase update and improvements

This commit is contained in:
thakares committed 2025-05-24 22:43:39 +05:30
commit 23a597a952
1052 files changed
+12015

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/nx9-dns-server.iml" filepath="$PROJECT_DIR$/.idea/nx9-dns-server.iml" />
</modules>
</component>
</project>
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+119
View File
@@ -0,0 +1,119 @@
## DNS Server Algorithm
**1. Server Initialization**
```rust
1.1 Load configuration from environment variables
1.2 Initialize logging system
1.3 Create SQLite database connection
1.4 Initialize cache with NS records
1.5 Start periodic cache cleanup task
1.6 Bind UDP and TCP sockets on specified port
```
**2. Query Handling Flow**
```
Start
│
▼
Receive DNS Query
│
▼
Parse Query Header/Question
│
▼
Check Cache for Domain Record
┌───────┴───────┐
▼ ▼
Cache Hit Cache Miss
│ │
▼ ▼
Build Response Query Database
│
▼
Check Authoritative Flag
┌────────┴────────┐
▼ ▼
Record Found Record Not Found
│ │
▼ ▼
Build Response Forward to Resolvers
│ │
▼ ▼
Add DNSSEC ▼
Signatures Receive Forwarded Response
│ │
▼ ▼
Send Response to Client
│
▼
End
```
**3. DNSSEC Signing Process**
```rust
3.1 Load DNSSEC key from configured file
3.2 For each relevant DNS record:
3.2.1 Generate RRSIG record
3.2.2 Encode signature using Base64
3.2.3 Calculate key tag and signature expiration
3.3 Add RRSIG records to DNS response
3.4 Include DNSKEY records in authority section
```
**4. Response Generation Logic**
```
4.1 Create response header with:
- Original query ID
- QR flag set to response
- Authoritative Answer flag
- Appropriate response code (NOERROR/NXDOMAIN)
4.2 Add original question section
4.3 Populate answer section with:
- Resource records from cache/database
- TTL values from configuration
4.4 Add authority section with:
- NS records
- DS records for DNSSEC
4.5 Include additional section with:
- A records for NS names
- DNSKEY records when applicable
```
## Key Data Flow Components
| Component | Purpose | Implementation Details |
| :-- | :-- | :-- |
| `DnsCache` | Response caching | Mutex-protected HashMap with TTL |
| `ServerConfig` | Runtime configuration | Environment variables parsing |
| `rusqlite` | Persistent storage | SQLite database with DNS records |
| `tokio` | Async I/O handling | UDP/TCP listeners with task spawning |
| `DNSSEC` | Response signing | RSA-SHA256 with preloaded keys |
## Error Handling Strategy
```rust
- Use custom DnsError enum with thiserror crate
- Graceful shutdown on SIGINT
- Automatic cache cleanup every 5 minutes
- Fallback to forwarding when local resolution fails
- Comprehensive logging at all stages
```
The server implements RFC 1035 (DNS) and RFC 4034 (DNSSEC) specifications with a focus on:
1. Async I/O using Tokio runtime
2. Thread-safe caching with atomic reference counting
3. Configurable forwarding and fallback mechanisms
4. DNSSEC signing capability for authoritative responses
5. SQLite-based record storage with schema versioning
<div style="text-align: center">⁂</div>
+172
View File
@@ -0,0 +1,172 @@
# 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
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
---
## API Reference
#### Get all items
```http
GET /api/items
```
| Parameter | Type | Description |
| :-------- | :------- | :------------------------- |
| `api_key` | `string` | **Required**. Your API key |
#### Get item
```http
GET /api/items/${id}
```
| Parameter | Type | Description |
| :-------- | :------- | :-------------------------------- |
| `id` | `string` | **Required**. Id of item to fetch |
#### add(num1, num2)
Takes two numbers and returns the sum.
+253
View File
@@ -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.
Generated
+881
View File
@@ -0,0 +1,881 @@
# 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 = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[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 = "anstream"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
dependencies = [
"anstyle",
"once_cell",
"windows-sys 0.59.0",
]
[[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.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[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.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0"
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 = "colorchoice"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "env_filter"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"jiff",
"log",
]
[[package]]
name = "fallible-iterator"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "hashbrown"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
dependencies = [
"foldhash",
]
[[package]]
name = "hashlink"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
dependencies = [
"hashbrown",
]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[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 = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "jiff"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
]
[[package]]
name = "jiff-static"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[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.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "947e6816f7825b2b45027c2c32e7085da9934defa535de4a6a46b10a4d5257fa"
dependencies = [
"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 = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[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",
"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 = "nx9_dns_server"
version = "0.1.0"
dependencies = [
"base64",
"chrono",
"env_logger",
"hex",
"log",
"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 = "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 = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"portable-atomic",
]
[[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 = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
dependencies = [
"bitflags",
]
[[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.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a22715a5d6deef63c637207afbe68d0c72c3f8d0022d7cf9714c442d6157606b"
dependencies = [
"bitflags",
"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 = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[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 = "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 = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
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 = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[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 = "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"
+15
View File
@@ -0,0 +1,15 @@
# Cargo.toml
[package]
name = "nx9_dns_server"
version = "0.1.0"
edition = "2021"
[dependencies]
rusqlite = "0.35.0"
log = "0.4.27"
thiserror = "2.0.12"
env_logger = "0.11.8"
tokio = { version = "1.44", features = ["full"] }
hex = "0.4.3"
base64 = "0.22.1"
chrono = "0.4.41"
+57
View File
@@ -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"]
+24
View File
@@ -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
Binary file not shown.
+1634
View File
File diff suppressed because it is too large. Load diff
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

+575
View File
@@ -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 [Your Name or Organization].
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.
+14
View File
@@ -0,0 +1,14 @@
sudo chmod +x /var/dns-server/preprocess-key.sh
sudo -u dnsuser /var/dns-server/preprocess-key.sh
sudo chown dnsuser:dnsuser /var/dns-server/soa-update.sh
sudo chmod +x /var/dns-server/soa-update.sh
sudo -u dnsuser /var/dns-server/soa-update.sh
sudo cat /var/dns-server/processed.key # Verify output format
sudo systemctl stop dns-server.service
sudo cp /home/sunil/apps/bzo-ddns/dns_server /var/dns-server/dns_server
sudo chown dnsuser:dnsuser /var/dns-server
sudo systemctl daemon-reload
sudo systemctl restart dns-server.service
sudo systemctl status dns-server.service
Executable
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
set -e
# Paths
SRC_BIN="/home/sunil/apps/bzo-ddns/dns_server"
DEST_DIR="/var/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
Executable
+9
View File
@@ -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
+36
View File
@@ -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.bzo.in', 3600),
('admin.bzo.in', 'A', '60.254.61.33', 3600),
('api.bzo.in', 'A', '60.254.61.33', 3600),
('bzo.in', 'A', '60.254.61.33', 3600),
('bzo.in', 'MX', '10 mail.bzo.in', 3600),
('bzo.in', 'NS', 'ns1.bzo.in', 3600),
('bzo.in', 'NS', 'ns2.bzo.in', 3600), -- This will now work with the new schema
('bzo.in', 'SOA', 'ns1.bzo.in hostmaster.bzo.in 1 10800 3600 604800 86400', 3600),
('bzo.in', 'TXT', '"v=spf1 a mx ~all"', 3600),
('ddns.bzo.in', 'A', '60.254.61.33', 3600),
('ns1.bzo.in', 'A', '60.254.61.33', 3600),
('ns2.bzo.in', 'A', '60.254.61.33', 3600),
('www.bzo.in', 'A', '60.254.61.33', 3600);
COMMIT;
+33
View File
@@ -0,0 +1,33 @@
[Unit]
Description=DNS Server
After=network.target
[Service]
User=dnsuser
Group=dnsuser
WorkingDirectory=/var/dns-server
ExecStart=/var/dns-server/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/dns-server/dns.db
Environment=DNS_NS_RECORDS=ns1.bzo.in.,ns2.bzo.in.
Environment=DNS_AUTHORITATIVE=1
Environment=DNS_CACHE_TTL=300
Environment=RUST_LOG=info
Environment=DNS_DEFAULT_DOMAIN=bzo.in
Environment=DNS_DEFAULT_IP=60.254.61.33
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/dns-server
ProtectSystem=full
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
Executable
+44
View File
@@ -0,0 +1,44 @@
#!/bin/bash
DOMAIN="bzo.in"
DNS_SERVER="192.168.1.200"
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 ==="
+102
View File
@@ -0,0 +1,102 @@
//! DNS cache implementation.
//!
//! This module provides a simple in-memory cache for DNS records to improve
//! performance by avoiding repeated database lookups for frequently accessed domains.
#![allow(dead_code)]
#[allow(unused_variables)]
use std::{
collections::HashMap,
sync::{Arc, Mutex, OnceLock},
time::{Duration, SystemTime},
};
use log::debug;
/// Interval for cleaning up expired cache entries (in seconds).
pub const CACHE_CLEANUP_INTERVAL: Duration = Duration::from_secs(300);
/// Global cache instance.
pub static CACHE: OnceLock<DnsCache> = 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<Mutex<HashMap<String, CacheEntry>>>,
/// List of NS records for zones this server is authoritative for.
pub ns_records: Vec<String>,
}
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<String>) -> 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");
}
}
+117
View File
@@ -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<String>,
/// 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<SocketAddr>,
/// List of DS records for DNSSEC.
pub ds_records: Vec<String>,
/// List of DNSKEY records for DNSSEC.
pub dnskey_records: Vec<String>,
}
impl ServerConfig {
/// Load server configuration from environment variables.
///
/// # Returns
/// A `Result` containing either the loaded `ServerConfig` or a `DnsError`.
pub fn from_env() -> Result<Self, DnsError> {
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(|_| "<your-public-ip4-here>".into()),
ds_records: vec![
"yourdomain.tld. IN DS 24550 8 2 1F21CA282945434EE0662805430599CB2A6C479D9F934087150901CE2DA580A0".to_string()
],
dnskey_records,
forwarders,
})
}
}
+224
View File
@@ -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<String>,
/// SOA record for the zone, if available.
pub soa_record: Option<String>,
}
/// 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<ZoneInfo> {
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<ZoneInfo> {
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
}
+1229
View File
File diff suppressed because it is too large. Load diff
+39
View File
@@ -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,
}
+39
View File
@@ -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,
}
+190
View File
@@ -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<u8>,
src: SocketAddr,
socket: Arc<UdpSocket>,
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(())
}
+23
View File
@@ -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;
+69
View File
@@ -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,
}
}
+427
View File
@@ -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<String> {
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<u16> {
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<u8> {
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<u32, DnsError> {
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<u16> {
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
}
+1
View File
@@ -0,0 +1 @@
{"rustc_fingerprint":18002557596678582880,"outputs":{"2063776225603076451":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/sunil/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\nfmt_debug=\"full\"\noverflow_checks\npanic=\"unwind\"\nproc_macro\nrelocation_model=\"pic\"\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_feature=\"x87\"\ntarget_has_atomic\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_has_atomic_equal_alignment=\"16\"\ntarget_has_atomic_equal_alignment=\"32\"\ntarget_has_atomic_equal_alignment=\"64\"\ntarget_has_atomic_equal_alignment=\"8\"\ntarget_has_atomic_equal_alignment=\"ptr\"\ntarget_has_atomic_load_store\ntarget_has_atomic_load_store=\"16\"\ntarget_has_atomic_load_store=\"32\"\ntarget_has_atomic_load_store=\"64\"\ntarget_has_atomic_load_store=\"8\"\ntarget_has_atomic_load_store=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_thread_local\ntarget_vendor=\"unknown\"\nub_checks\nunix\n","stderr":""},"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.85.0 (4d91de4e4 2025-02-17)\nbinary: rustc\ncommit-hash: 4d91de4e48198da2e33413efdcd9cd2cc0c46688\ncommit-date: 2025-02-17\nhost: x86_64-unknown-linux-gnu\nrelease: 1.85.0\nLLVM version: 19.1.7\n","stderr":""}},"successes":{}}
+3
View File
@@ -0,0 +1,3 @@
Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by cargo.
# For information about cache directory tags see https://bford.info/cachedir/
View File
Whitespace-only changes.
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
f3165bca0a5fe479
@@ -0,0 +1 @@
{"rustc":8277423686421874925,"features":"[\"perf-literal\", \"std\"]","declared_features":"[\"default\", \"logging\", \"perf-literal\", \"std\"]","target":7534583537114156500,"profile":15657897354478470176,"path":6777680782445871586,"deps":[[3129130049864710036,"memchr",false,14960082680285793101]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/aho-corasick-458a8eb53f553e15/dep-lib-aho_corasick","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
381080c2559ec32c
@@ -0,0 +1 @@
{"rustc":8277423686421874925,"features":"[\"perf-literal\", \"std\"]","declared_features":"[\"default\", \"logging\", \"perf-literal\", \"std\"]","target":7534583537114156500,"profile":2241668132362809309,"path":6777680782445871586,"deps":[[3129130049864710036,"memchr",false,5377587700890516408]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/aho-corasick-512a0353399959b4/dep-lib-aho_corasick","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
7d229901a26f5080
@@ -0,0 +1 @@
{"rustc":8277423686421874925,"features":"[\"auto\", \"wincon\"]","declared_features":"[\"auto\", \"default\", \"test\", \"wincon\"]","target":11278316191512382530,"profile":17255432589167795725,"path":5267557481979336949,"deps":[[4858255257716900954,"anstyle",false,1195121669556831546],[6062327512194961595,"is_terminal_polyfill",false,9342045378381741919],[8605544941055515999,"anstyle_parse",false,1593238180510360833],[9179982570249329464,"anstyle_query",false,14314701584430781856],[16319705629219006414,"colorchoice",false,14935866763957947330],[17716308468579268865,"utf8parse",false,6480995639111405339]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anstream-476e0bc592a14288/dep-lib-anstream","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
2aa56461ef4996df
@@ -0,0 +1 @@
{"rustc":8277423686421874925,"features":"[\"auto\", \"wincon\"]","declared_features":"[\"auto\", \"default\", \"test\", \"wincon\"]","target":11278316191512382530,"profile":6996883392558192706,"path":5267557481979336949,"deps":[[4858255257716900954,"anstyle",false,3419040232658842972],[6062327512194961595,"is_terminal_polyfill",false,1738963671796250148],[8605544941055515999,"anstyle_parse",false,6266856327409153533],[9179982570249329464,"anstyle_query",false,13480709300906088603],[16319705629219006414,"colorchoice",false,15266426214031929042],[17716308468579268865,"utf8parse",false,7816298531699643450]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anstream-812c3563ccd78e68/dep-lib-anstream","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
5c35660cfade722f
@@ -0,0 +1 @@
{"rustc":8277423686421874925,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"std\"]","target":6165884447290141869,"profile":6996883392558192706,"path":3214559300204461204,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anstyle-6a69816386652f48/dep-lib-anstyle","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
3a854e91d3ec9510
@@ -0,0 +1 @@
{"rustc":8277423686421874925,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"std\"]","target":6165884447290141869,"profile":17255432589167795725,"path":3214559300204461204,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anstyle-abf609cfb869b01f/dep-lib-anstyle","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
fdb5a165a058f856
@@ -0,0 +1 @@
{"rustc":8277423686421874925,"features":"[\"default\", \"utf8\"]","declared_features":"[\"core\", \"default\", \"utf8\"]","target":10225663410500332907,"profile":6996883392558192706,"path":9923905818475643453,"deps":[[17716308468579268865,"utf8parse",false,7816298531699643450]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anstyle-parse-45c21a3d8e9d7e83/dep-lib-anstyle_parse","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
01e5e80faf511c16
@@ -0,0 +1 @@
{"rustc":8277423686421874925,"features":"[\"default\", \"utf8\"]","declared_features":"[\"core\", \"default\", \"utf8\"]","target":10225663410500332907,"profile":17255432589167795725,"path":9923905818475643453,"deps":[[17716308468579268865,"utf8parse",false,6480995639111405339]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anstyle-parse-fc720f72f3cfdc0b/dep-lib-anstyle_parse","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
a0d5b79d3f09a8c6
@@ -0,0 +1 @@
{"rustc":8277423686421874925,"features":"[]","declared_features":"[]","target":10705714425685373190,"profile":17255432589167795725,"path":11801876219804316293,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anstyle-query-3d65e11fd2678954/dep-lib-anstyle_query","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
9bd8cf70af1915bb
@@ -0,0 +1 @@
{"rustc":8277423686421874925,"features":"[]","declared_features":"[]","target":10705714425685373190,"profile":6996883392558192706,"path":11801876219804316293,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anstyle-query-ddc4e37e616485b7/dep-lib-anstyle_query","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
8be1dfd5a95c26a4
@@ -0,0 +1 @@
{"rustc":8277423686421874925,"features":"[]","declared_features":"[]","target":6962977057026645649,"profile":2225463790103693989,"path":15504328327543547035,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/autocfg-bf362afc358f4bcf/dep-lib-autocfg","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
da448c2da380e1d8
@@ -0,0 +1 @@
{"rustc":8277423686421874925,"features":"[\"alloc\", \"default\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"std\"]","target":13060062996227388079,"profile":15657897354478470176,"path":7782051874790542920,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/base64-c687f77308be5e43/dep-lib-base64","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
ddfdbb6c8064865c
@@ -0,0 +1 @@
{"rustc":8277423686421874925,"features":"[\"alloc\", \"default\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"std\"]","target":13060062996227388079,"profile":2241668132362809309,"path":7782051874790542920,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/base64-dd90e39b716d6925/dep-lib-base64","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
3f2384117702e714
@@ -0,0 +1 @@
{"rustc":8277423686421874925,"features":"[]","declared_features":"[\"arbitrary\", \"bytemuck\", \"compiler_builtins\", \"core\", \"example_generated\", \"rustc-dep-of-std\", \"serde\", \"std\"]","target":7691312148208718491,"profile":2241668132362809309,"path":17623579301503799727,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/bitflags-07d211d8a0ad1826/dep-lib-bitflags","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
13d6fb8616e65b45
@@ -0,0 +1 @@
{"rustc":8277423686421874925,"features":"[]","declared_features":"[\"arbitrary\", \"bytemuck\", \"compiler_builtins\", \"core\", \"example_generated\", \"rustc-dep-of-std\", \"serde\", \"std\"]","target":7691312148208718491,"profile":15657897354478470176,"path":17623579301503799727,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/bitflags-8a9dfde32bf45907/dep-lib-bitflags","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
07407d067098d321
@@ -0,0 +1 @@
{"rustc":8277423686421874925,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"extra-platforms\", \"serde\", \"std\"]","target":15971911772774047941,"profile":13827760451848848284,"path":11248016098080301350,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/bytes-27dc7e6c08f3be5e/dep-lib-bytes","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
Loaded 100 of 1052 files, more files were not shown because too many files have changed in this diff. Show more