dosev 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dosev-1.0.0/LICENSE +21 -0
- dosev-1.0.0/PKG-INFO +145 -0
- dosev-1.0.0/README.md +123 -0
- dosev-1.0.0/dosev/__init__.py +6 -0
- dosev-1.0.0/dosev/cli.py +81 -0
- dosev-1.0.0/dosev/config.py +295 -0
- dosev-1.0.0/dosev/resolver.py +2448 -0
- dosev-1.0.0/dosev/server.py +704 -0
- dosev-1.0.0/dosev/utils.py +46 -0
- dosev-1.0.0/dosev.egg-info/PKG-INFO +145 -0
- dosev-1.0.0/dosev.egg-info/SOURCES.txt +25 -0
- dosev-1.0.0/dosev.egg-info/dependency_links.txt +1 -0
- dosev-1.0.0/dosev.egg-info/entry_points.txt +2 -0
- dosev-1.0.0/dosev.egg-info/requires.txt +12 -0
- dosev-1.0.0/dosev.egg-info/top_level.txt +1 -0
- dosev-1.0.0/pyproject.toml +42 -0
- dosev-1.0.0/setup.cfg +4 -0
- dosev-1.0.0/tests/test_cli.py +51 -0
- dosev-1.0.0/tests/test_config.py +34 -0
- dosev-1.0.0/tests/test_config_extra.py +188 -0
- dosev-1.0.0/tests/test_integration_upstreams.py +71 -0
- dosev-1.0.0/tests/test_resolver.py +169 -0
- dosev-1.0.0/tests/test_resolver_extra.py +118 -0
- dosev-1.0.0/tests/test_server.py +56 -0
- dosev-1.0.0/tests/test_server_extra.py +351 -0
- dosev-1.0.0/tests/test_utils.py +47 -0
- dosev-1.0.0/tests/test_utils_extra.py +61 -0
dosev-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mohammad Amin Kiani
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
dosev-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dosev
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Multi‑protocol DNS resolver with full server and config support
|
|
5
|
+
Author-email: Mohammad Amin Kiani <mohammadamin.k1390@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: dnspython>=2.4.0
|
|
11
|
+
Requires-Dist: aiohttp>=3.8.0
|
|
12
|
+
Requires-Dist: aioquic>=0.9.0
|
|
13
|
+
Requires-Dist: cryptography>=38.0.0
|
|
14
|
+
Requires-Dist: cachetools>=5.0.0
|
|
15
|
+
Requires-Dist: httpx>=0.23.0
|
|
16
|
+
Requires-Dist: prometheus-client>=0.14.0
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
19
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "test"
|
|
20
|
+
Requires-Dist: pytest-cov>=4.0; extra == "test"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# dosev – Multi‑Protocol DNS Resolver & Server
|
|
24
|
+
|
|
25
|
+
[](https://github.com/KianiDev/dosev/actions/workflows/ci.yml)
|
|
26
|
+
[](https://pypi.org/project/dosev/)
|
|
27
|
+
[](https://pypi.org/project/dosev/)
|
|
28
|
+
[](https://opensource.org/licenses/MIT)
|
|
29
|
+
|
|
30
|
+
**dosev** is a high‑performance, asynchronous DNS resolver and forwarding server that speaks **all major DNS protocols** – both as a client and as a server.
|
|
31
|
+
|
|
32
|
+
- **Client protocols**: DNS over UDP, TCP, TLS (DoT), HTTPS (DoH – HTTP/1.1, HTTP/2, HTTP/3), and QUIC (DoQ).
|
|
33
|
+
- **Server protocols**: UDP, TCP, TLS (DoT), HTTPS (DoH – HTTP/2 and HTTP/3).
|
|
34
|
+
- **Features**: EDNS0 (including Client Subnet), DNSSEC validation with automatic trust anchor updates, negative caching, optimistic caching (serve‑stale), blocklists, hosts overrides, upstream health checks, load balancing, Prometheus metrics, and more.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 🚀 Quick Start
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Install via pip
|
|
42
|
+
pip install dosev
|
|
43
|
+
|
|
44
|
+
# Create a minimal config file
|
|
45
|
+
mkdir -p config
|
|
46
|
+
cat > config/dosev.conf <<EOF
|
|
47
|
+
[server]
|
|
48
|
+
listen_ip = 0.0.0.0
|
|
49
|
+
listen_port = 53
|
|
50
|
+
|
|
51
|
+
[resolver]
|
|
52
|
+
upstream_dns = 1.1.1.1
|
|
53
|
+
protocol = udp
|
|
54
|
+
EOF
|
|
55
|
+
|
|
56
|
+
# Start the server
|
|
57
|
+
dosev --config config/dosev.conf
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
For detailed instructions, see the [Quick Start Guide](docs/quickstart.md).
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## ✨ Features
|
|
65
|
+
|
|
66
|
+
| Feature | Description |
|
|
67
|
+
|---------|-------------|
|
|
68
|
+
| **Multi‑protocol client** | Forward queries via UDP, TCP, TLS, HTTPS (HTTP/1.1, HTTP/2, HTTP/3), and QUIC. |
|
|
69
|
+
| **Multi‑protocol server** | Listen on UDP, TCP, TLS, and HTTPS (HTTP/2 & HTTP/3). |
|
|
70
|
+
| **DNSSEC validation** | Validate responses with a built‑in root trust anchor (auto‑updated from IANA). |
|
|
71
|
+
| **Caching** | Positive and negative caching with configurable TTLs; optimistic caching (serve‑stale). |
|
|
72
|
+
| **Blocklists** | Filter domains using local files or remote lists (automatically refreshed). |
|
|
73
|
+
| **Hosts overrides** | Custom A/AAAA records for local name resolution. |
|
|
74
|
+
| **EDNS0 & Client Subnet** | Pass client subnet to upstreams for geo‑optimised responses. |
|
|
75
|
+
| **Load balancing & health checks** | Distribute queries across multiple upstreams; automatically exclude unhealthy ones. |
|
|
76
|
+
| **Rate limiting** | Token‑bucket limiter per client IP. |
|
|
77
|
+
| **Rebinding protection** | Strip or block private IP addresses from responses. |
|
|
78
|
+
| **Metrics** | Prometheus‑compatible metrics (requests, errors, latency, cache stats). |
|
|
79
|
+
| **Privilege dropping** | Drop root privileges after binding to privileged ports. |
|
|
80
|
+
| **Cross‑platform** | Works on Linux, macOS, and Windows. |
|
|
81
|
+
| **Configuration** | Single INI file; can be reloaded without restarting. |
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 📖 Documentation
|
|
86
|
+
|
|
87
|
+
- [Quick Start](docs/quickstart.md) – get up and running in minutes.
|
|
88
|
+
- [Configuration Reference](docs/configuration.md) – all available options explained.
|
|
89
|
+
- [Deployment Guide](docs/deployment.md) – systemd, Docker, and production tuning.
|
|
90
|
+
- [Contributing](docs/CONTRIBUTING.md) – how to help improve dosev.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 🔧 Configuration Example
|
|
95
|
+
|
|
96
|
+
```ini
|
|
97
|
+
[server]
|
|
98
|
+
listen_ip = 0.0.0.0
|
|
99
|
+
listen_port = 53
|
|
100
|
+
|
|
101
|
+
[resolver]
|
|
102
|
+
upstream_dns = 1.1.1.1
|
|
103
|
+
protocol = udp
|
|
104
|
+
|
|
105
|
+
[security]
|
|
106
|
+
dnssec_enabled = true
|
|
107
|
+
auto_update_trust_anchor = true
|
|
108
|
+
|
|
109
|
+
[blocklists]
|
|
110
|
+
enabled = true
|
|
111
|
+
urls = https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
|
|
112
|
+
action = NXDOMAIN
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
See the full [configuration reference](docs/configuration.md) for all options.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 🧪 Running Tests
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pip install dosev[test]
|
|
123
|
+
pytest --cov=dosev --cov-report=term-missing
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 🤝 Contributing
|
|
129
|
+
|
|
130
|
+
We welcome contributions! Please read our [Contributing Guide](docs/CONTRIBUTING.md) before opening issues or pull requests.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 📄 License
|
|
135
|
+
|
|
136
|
+
This project is licensed under the MIT License – see the [LICENSE](LICENSE) file for details.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 🙏 Acknowledgements
|
|
141
|
+
|
|
142
|
+
- [dnspython](https://www.dnspython.org/) – for DNS message handling.
|
|
143
|
+
- [aiohttp](https://docs.aiohttp.org/) – for HTTP/1.1 and HTTP/2 support.
|
|
144
|
+
- [aioquic](https://github.com/aiortc/aioquic) – for HTTP/3 and QUIC support.
|
|
145
|
+
- [Prometheus](https://prometheus.io/) – for metrics.
|
dosev-1.0.0/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# dosev – Multi‑Protocol DNS Resolver & Server
|
|
2
|
+
|
|
3
|
+
[](https://github.com/KianiDev/dosev/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/dosev/)
|
|
5
|
+
[](https://pypi.org/project/dosev/)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
**dosev** is a high‑performance, asynchronous DNS resolver and forwarding server that speaks **all major DNS protocols** – both as a client and as a server.
|
|
9
|
+
|
|
10
|
+
- **Client protocols**: DNS over UDP, TCP, TLS (DoT), HTTPS (DoH – HTTP/1.1, HTTP/2, HTTP/3), and QUIC (DoQ).
|
|
11
|
+
- **Server protocols**: UDP, TCP, TLS (DoT), HTTPS (DoH – HTTP/2 and HTTP/3).
|
|
12
|
+
- **Features**: EDNS0 (including Client Subnet), DNSSEC validation with automatic trust anchor updates, negative caching, optimistic caching (serve‑stale), blocklists, hosts overrides, upstream health checks, load balancing, Prometheus metrics, and more.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 🚀 Quick Start
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Install via pip
|
|
20
|
+
pip install dosev
|
|
21
|
+
|
|
22
|
+
# Create a minimal config file
|
|
23
|
+
mkdir -p config
|
|
24
|
+
cat > config/dosev.conf <<EOF
|
|
25
|
+
[server]
|
|
26
|
+
listen_ip = 0.0.0.0
|
|
27
|
+
listen_port = 53
|
|
28
|
+
|
|
29
|
+
[resolver]
|
|
30
|
+
upstream_dns = 1.1.1.1
|
|
31
|
+
protocol = udp
|
|
32
|
+
EOF
|
|
33
|
+
|
|
34
|
+
# Start the server
|
|
35
|
+
dosev --config config/dosev.conf
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
For detailed instructions, see the [Quick Start Guide](docs/quickstart.md).
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## ✨ Features
|
|
43
|
+
|
|
44
|
+
| Feature | Description |
|
|
45
|
+
|---------|-------------|
|
|
46
|
+
| **Multi‑protocol client** | Forward queries via UDP, TCP, TLS, HTTPS (HTTP/1.1, HTTP/2, HTTP/3), and QUIC. |
|
|
47
|
+
| **Multi‑protocol server** | Listen on UDP, TCP, TLS, and HTTPS (HTTP/2 & HTTP/3). |
|
|
48
|
+
| **DNSSEC validation** | Validate responses with a built‑in root trust anchor (auto‑updated from IANA). |
|
|
49
|
+
| **Caching** | Positive and negative caching with configurable TTLs; optimistic caching (serve‑stale). |
|
|
50
|
+
| **Blocklists** | Filter domains using local files or remote lists (automatically refreshed). |
|
|
51
|
+
| **Hosts overrides** | Custom A/AAAA records for local name resolution. |
|
|
52
|
+
| **EDNS0 & Client Subnet** | Pass client subnet to upstreams for geo‑optimised responses. |
|
|
53
|
+
| **Load balancing & health checks** | Distribute queries across multiple upstreams; automatically exclude unhealthy ones. |
|
|
54
|
+
| **Rate limiting** | Token‑bucket limiter per client IP. |
|
|
55
|
+
| **Rebinding protection** | Strip or block private IP addresses from responses. |
|
|
56
|
+
| **Metrics** | Prometheus‑compatible metrics (requests, errors, latency, cache stats). |
|
|
57
|
+
| **Privilege dropping** | Drop root privileges after binding to privileged ports. |
|
|
58
|
+
| **Cross‑platform** | Works on Linux, macOS, and Windows. |
|
|
59
|
+
| **Configuration** | Single INI file; can be reloaded without restarting. |
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 📖 Documentation
|
|
64
|
+
|
|
65
|
+
- [Quick Start](docs/quickstart.md) – get up and running in minutes.
|
|
66
|
+
- [Configuration Reference](docs/configuration.md) – all available options explained.
|
|
67
|
+
- [Deployment Guide](docs/deployment.md) – systemd, Docker, and production tuning.
|
|
68
|
+
- [Contributing](docs/CONTRIBUTING.md) – how to help improve dosev.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 🔧 Configuration Example
|
|
73
|
+
|
|
74
|
+
```ini
|
|
75
|
+
[server]
|
|
76
|
+
listen_ip = 0.0.0.0
|
|
77
|
+
listen_port = 53
|
|
78
|
+
|
|
79
|
+
[resolver]
|
|
80
|
+
upstream_dns = 1.1.1.1
|
|
81
|
+
protocol = udp
|
|
82
|
+
|
|
83
|
+
[security]
|
|
84
|
+
dnssec_enabled = true
|
|
85
|
+
auto_update_trust_anchor = true
|
|
86
|
+
|
|
87
|
+
[blocklists]
|
|
88
|
+
enabled = true
|
|
89
|
+
urls = https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
|
|
90
|
+
action = NXDOMAIN
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
See the full [configuration reference](docs/configuration.md) for all options.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 🧪 Running Tests
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pip install dosev[test]
|
|
101
|
+
pytest --cov=dosev --cov-report=term-missing
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 🤝 Contributing
|
|
107
|
+
|
|
108
|
+
We welcome contributions! Please read our [Contributing Guide](docs/CONTRIBUTING.md) before opening issues or pull requests.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 📄 License
|
|
113
|
+
|
|
114
|
+
This project is licensed under the MIT License – see the [LICENSE](LICENSE) file for details.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 🙏 Acknowledgements
|
|
119
|
+
|
|
120
|
+
- [dnspython](https://www.dnspython.org/) – for DNS message handling.
|
|
121
|
+
- [aiohttp](https://docs.aiohttp.org/) – for HTTP/1.1 and HTTP/2 support.
|
|
122
|
+
- [aioquic](https://github.com/aiortc/aioquic) – for HTTP/3 and QUIC support.
|
|
123
|
+
- [Prometheus](https://prometheus.io/) – for metrics.
|
dosev-1.0.0/dosev/cli.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import argparse
|
|
4
|
+
from .config import load_config, _default_log_dir
|
|
5
|
+
from .server import run_server_sync
|
|
6
|
+
|
|
7
|
+
def main() -> None:
|
|
8
|
+
parser = argparse.ArgumentParser(description="dosev DNS server")
|
|
9
|
+
parser.add_argument("--config", "-c", default="config/dosev.conf",
|
|
10
|
+
help="Path to configuration file (default: config/dosev.conf)")
|
|
11
|
+
parser.add_argument("--check-config", action="store_true",
|
|
12
|
+
help="Validate the configuration and exit without starting the server")
|
|
13
|
+
args = parser.parse_args()
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
config = load_config(args.config)
|
|
17
|
+
except Exception as e:
|
|
18
|
+
print(f"Failed to load config: {e}", file=sys.stderr)
|
|
19
|
+
sys.exit(1)
|
|
20
|
+
|
|
21
|
+
if args.check_config:
|
|
22
|
+
print("Configuration is valid.")
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
run_server_sync(
|
|
26
|
+
listen_ip=config["listen_ip"],
|
|
27
|
+
listen_port=config["listen_port"],
|
|
28
|
+
upstream_dns=config["upstream_dns"],
|
|
29
|
+
protocol=config["protocol"],
|
|
30
|
+
verbose=config.get("verbose", False),
|
|
31
|
+
blocklists=config.get("blocklists"),
|
|
32
|
+
disable_ipv6=config.get("disable_ipv6", False),
|
|
33
|
+
dns_cache_ttl=config.get("dns_cache_ttl", 300),
|
|
34
|
+
dns_cache_max_size=config.get("dns_cache_max_size", 1024),
|
|
35
|
+
dns_negative_cache_ttl=config.get("dns_negative_cache_ttl", 5),
|
|
36
|
+
dns_logging_enabled=config.get("dns_logging_enabled", False),
|
|
37
|
+
dns_log_retention_days=config.get("dns_log_retention_days", 7),
|
|
38
|
+
dns_log_dir=config.get("dns_log_dir", _default_log_dir()),
|
|
39
|
+
dns_log_prefix=config.get("dns_log_prefix", "dns-log"),
|
|
40
|
+
dns_pinned_certs=config.get("dns_pinned_certs", {}),
|
|
41
|
+
dnssec_enabled=config.get("dnssec_enabled", False),
|
|
42
|
+
auto_update_trust_anchor=config.get("auto_update_trust_anchor", True), # NEW
|
|
43
|
+
trust_anchors_file=config.get("trust_anchors_file", ""),
|
|
44
|
+
metrics_enabled=config.get("metrics_enabled", False),
|
|
45
|
+
metrics_port=config.get("metrics_port", 8000),
|
|
46
|
+
uvloop_enable=config.get("uvloop_enable", False),
|
|
47
|
+
upstream_retries=config.get("upstream_retries", 2),
|
|
48
|
+
upstream_udp_timeout=config.get("upstream_udp_timeout", 2.0),
|
|
49
|
+
upstream_tcp_timeout=config.get("upstream_tcp_timeout", 5.0),
|
|
50
|
+
upstream_doh_timeout=config.get("upstream_doh_timeout", 5.0),
|
|
51
|
+
rate_limit_rps=config.get("rate_limit_rps", 0.0),
|
|
52
|
+
rate_limit_burst=config.get("rate_limit_burst", 0.0),
|
|
53
|
+
upstreams=config.get("upstreams", []),
|
|
54
|
+
optimistic_cache_enabled=config.get("optimistic_cache_enabled", False),
|
|
55
|
+
optimistic_stale_max_age=config.get("optimistic_stale_max_age", 86400),
|
|
56
|
+
optimistic_stale_response_ttl=config.get("optimistic_stale_response_ttl", 30),
|
|
57
|
+
dns_privilege_drop_user=config.get("dns_privilege_drop_user", ""),
|
|
58
|
+
dns_privilege_drop_group=config.get("dns_privilege_drop_group", ""),
|
|
59
|
+
dns_chroot_dir=config.get("dns_chroot_dir", ""),
|
|
60
|
+
dns_rebind_protection=config.get("dns_rebind_protection", False),
|
|
61
|
+
dns_rebind_action=config.get("dns_rebind_action", "strip"),
|
|
62
|
+
dns_ecs_enabled=config.get("dns_ecs_enabled", True),
|
|
63
|
+
dns_max_payload=config.get("dns_max_payload", 4096),
|
|
64
|
+
dns_enable_dot=config.get("dns_enable_dot", False),
|
|
65
|
+
dns_dot_port=config.get("dns_dot_port", 853),
|
|
66
|
+
dns_dot_cert_file=config.get("dns_dot_cert_file", ""),
|
|
67
|
+
dns_dot_key_file=config.get("dns_dot_key_file", ""),
|
|
68
|
+
dns_enable_doh=config.get("dns_enable_doh", False),
|
|
69
|
+
dns_doh_port=config.get("dns_doh_port", 443),
|
|
70
|
+
dns_doh_cert_file=config.get("dns_doh_cert_file", ""),
|
|
71
|
+
dns_doh_key_file=config.get("dns_doh_key_file", ""),
|
|
72
|
+
dns_doh_path=config.get("dns_doh_path", "/dns-query"),
|
|
73
|
+
pool_max_size=config.get("pool_max_size", 5),
|
|
74
|
+
pool_idle_timeout=config.get("pool_idle_timeout", 60.0),
|
|
75
|
+
doh_version=config.get("doh_version", "auto"),
|
|
76
|
+
doh_auto_cache_ttl=config.get("doh_auto_cache_ttl", 3600),
|
|
77
|
+
bootstrap=config.get("bootstrap", {"servers": [], "timeout": 2.0, "retries": 2}),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
main()
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import configparser
|
|
3
|
+
import warnings
|
|
4
|
+
from typing import Dict, Any, List, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _default_log_dir() -> str:
|
|
8
|
+
if os.name == 'nt':
|
|
9
|
+
return os.path.join(os.getenv('LOCALAPPDATA') or os.path.expanduser('~'), 'dosev', 'logs')
|
|
10
|
+
return '/var/log/dosev'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _validate_and_warn(config: Dict[str, Any]) -> None:
|
|
14
|
+
listen_port = config.get('listen_port', 53)
|
|
15
|
+
if not isinstance(listen_port, int) or not 1 <= listen_port <= 65535:
|
|
16
|
+
raise ValueError('listen_port must be between 1 and 65535')
|
|
17
|
+
|
|
18
|
+
dns_max_payload = config.get('dns_max_payload', 4096)
|
|
19
|
+
if not isinstance(dns_max_payload, int) or not 512 <= dns_max_payload <= 4096:
|
|
20
|
+
raise ValueError('dns_max_payload must be between 512 and 4096')
|
|
21
|
+
|
|
22
|
+
protocol = config.get('protocol', 'udp')
|
|
23
|
+
if protocol not in {'udp', 'tcp', 'tls', 'https', 'quic'}:
|
|
24
|
+
raise ValueError('protocol must be one of: udp, tcp, tls, https, quic')
|
|
25
|
+
|
|
26
|
+
if config.get('dns_enable_dot', False) and not (config.get('dns_dot_cert_file') and config.get('dns_dot_key_file')):
|
|
27
|
+
raise ValueError('dns_enable_dot requires dns_dot_cert_file and dns_dot_key_file')
|
|
28
|
+
|
|
29
|
+
if config.get('dns_enable_doh', False) and not (config.get('dns_doh_cert_file') and config.get('dns_doh_key_file')):
|
|
30
|
+
raise ValueError('dns_enable_doh requires dns_doh_cert_file and dns_doh_key_file')
|
|
31
|
+
|
|
32
|
+
if config.get('dns_enable_dot', False) or config.get('dns_enable_doh', False):
|
|
33
|
+
if config.get('protocol') == 'udp':
|
|
34
|
+
warnings.warn('Secure listeners are enabled while the main resolver protocol is udp; this is usually intentional but verify your upstream defaults.', RuntimeWarning)
|
|
35
|
+
|
|
36
|
+
rate_limit_rps = config.get('rate_limit_rps', 0.0)
|
|
37
|
+
if rate_limit_rps < 0:
|
|
38
|
+
raise ValueError('rate_limit_rps must be non-negative')
|
|
39
|
+
|
|
40
|
+
rate_limit_burst = config.get('rate_limit_burst', 0.0)
|
|
41
|
+
if rate_limit_burst < 0:
|
|
42
|
+
raise ValueError('rate_limit_burst must be non-negative')
|
|
43
|
+
|
|
44
|
+
if config.get('metrics_enabled', False) and config.get('metrics_port', 8000) == 53:
|
|
45
|
+
warnings.warn('Metrics are enabled on port 53; this may conflict with DNS traffic.', RuntimeWarning)
|
|
46
|
+
|
|
47
|
+
if config.get('doh_version', 'auto') not in {'auto', '1.1', '2', '3'}:
|
|
48
|
+
raise ValueError('doh_version must be auto, 1.1, 2, or 3')
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def load_config(path: str = 'config/dosev.conf') -> Dict[str, Any]:
|
|
52
|
+
config = configparser.ConfigParser()
|
|
53
|
+
if not os.path.exists(path):
|
|
54
|
+
return {
|
|
55
|
+
'listen_ip': '0.0.0.0',
|
|
56
|
+
'listen_port': 53,
|
|
57
|
+
'upstream_dns': '1.1.1.1',
|
|
58
|
+
'protocol': 'udp',
|
|
59
|
+
'verbose': False,
|
|
60
|
+
'disable_ipv6': False,
|
|
61
|
+
'strip_ipv6_records': None, # None = use disable_ipv6 value
|
|
62
|
+
'dns_cache_ttl': 300,
|
|
63
|
+
'dns_cache_max_size': 1024,
|
|
64
|
+
'dns_negative_cache_ttl': 5,
|
|
65
|
+
'dns_logging_enabled': False,
|
|
66
|
+
'dns_log_retention_days': 7,
|
|
67
|
+
'dns_log_dir': _default_log_dir(),
|
|
68
|
+
'dns_log_prefix': 'dns-log',
|
|
69
|
+
'dns_pinned_certs': {},
|
|
70
|
+
'dns_ecs_enabled': True,
|
|
71
|
+
'dnssec_enabled': False,
|
|
72
|
+
'auto_update_trust_anchor': True,
|
|
73
|
+
'trust_anchors_file': '',
|
|
74
|
+
'metrics_enabled': False,
|
|
75
|
+
'metrics_port': 8000,
|
|
76
|
+
'uvloop_enable': False,
|
|
77
|
+
'upstream_retries': 2,
|
|
78
|
+
'upstream_udp_timeout': 2.0,
|
|
79
|
+
'upstream_tcp_timeout': 5.0,
|
|
80
|
+
'upstream_doh_timeout': 5.0,
|
|
81
|
+
'rate_limit_rps': 0.0,
|
|
82
|
+
'rate_limit_burst': 0.0,
|
|
83
|
+
'upstreams': [],
|
|
84
|
+
'optimistic_cache_enabled': False,
|
|
85
|
+
'optimistic_stale_max_age': 86400,
|
|
86
|
+
'optimistic_stale_response_ttl': 30,
|
|
87
|
+
'dns_privilege_drop_user': '',
|
|
88
|
+
'dns_privilege_drop_group': '',
|
|
89
|
+
'dns_chroot_dir': '',
|
|
90
|
+
'dns_rebind_protection': False,
|
|
91
|
+
'dns_rebind_action': 'strip',
|
|
92
|
+
'pool_max_size': 5,
|
|
93
|
+
'pool_idle_timeout': 60.0,
|
|
94
|
+
'doh_version': 'auto',
|
|
95
|
+
'doh_auto_cache_ttl': 3600,
|
|
96
|
+
'bootstrap': {'servers': ['1.1.1.1:53', '8.8.8.8:53'], 'timeout': 2.0, 'retries': 2},
|
|
97
|
+
'blocklists': {
|
|
98
|
+
'enabled': False,
|
|
99
|
+
'urls': [],
|
|
100
|
+
'interval_seconds': 86400,
|
|
101
|
+
'action': 'NXDOMAIN',
|
|
102
|
+
'local_blocklist_dir': 'blocklists',
|
|
103
|
+
'reload_on_change': True,
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
config.read(path)
|
|
108
|
+
|
|
109
|
+
listen_ip = config.get('server', 'listen_ip', fallback='0.0.0.0')
|
|
110
|
+
listen_port = config.getint('server', 'listen_port', fallback=53)
|
|
111
|
+
|
|
112
|
+
upstream_dns = config.get('resolver', 'upstream_dns', fallback='1.1.1.1')
|
|
113
|
+
protocol = config.get('resolver', 'protocol', fallback='udp').lower()
|
|
114
|
+
verbose = config.getboolean('resolver', 'verbose', fallback=False)
|
|
115
|
+
disable_ipv6 = config.getboolean('resolver', 'disable_ipv6', fallback=False)
|
|
116
|
+
dns_ecs_enabled = config.getboolean('resolver', 'dns_ecs_enabled', fallback=True)
|
|
117
|
+
dns_max_payload = config.getint('resolver', 'dns_max_payload', fallback=4096)
|
|
118
|
+
dns_enable_dot = config.getboolean('resolver', 'dns_enable_dot', fallback=False)
|
|
119
|
+
dns_dot_port = config.getint('resolver', 'dns_dot_port', fallback=853)
|
|
120
|
+
dns_dot_cert_file = config.get('resolver', 'dns_dot_cert_file', fallback='')
|
|
121
|
+
dns_dot_key_file = config.get('resolver', 'dns_dot_key_file', fallback='')
|
|
122
|
+
dns_enable_doh = config.getboolean('resolver', 'dns_enable_doh', fallback=False)
|
|
123
|
+
dns_doh_port = config.getint('resolver', 'dns_doh_port', fallback=443)
|
|
124
|
+
dns_doh_cert_file = config.get('resolver', 'dns_doh_cert_file', fallback='')
|
|
125
|
+
dns_doh_key_file = config.get('resolver', 'dns_doh_key_file', fallback='')
|
|
126
|
+
dns_doh_path = config.get('resolver', 'dns_doh_path', fallback='/dns-query')
|
|
127
|
+
strip_ipv6_records_raw = config.get('resolver', 'strip_ipv6_records', fallback=None)
|
|
128
|
+
if strip_ipv6_records_raw is None:
|
|
129
|
+
strip_ipv6_records = None
|
|
130
|
+
else:
|
|
131
|
+
strip_ipv6_records = config.getboolean('resolver', 'strip_ipv6_records', fallback=None)
|
|
132
|
+
|
|
133
|
+
dns_cache_ttl = config.getint('cache', 'ttl', fallback=300)
|
|
134
|
+
dns_cache_max_size = config.getint('cache', 'max_size', fallback=1024)
|
|
135
|
+
dns_negative_cache_ttl = config.getint('cache', 'negative_ttl', fallback=5)
|
|
136
|
+
|
|
137
|
+
upstream_udp_timeout = config.getfloat('timeouts', 'udp', fallback=2.0)
|
|
138
|
+
upstream_tcp_timeout = config.getfloat('timeouts', 'tcp', fallback=5.0)
|
|
139
|
+
upstream_doh_timeout = config.getfloat('timeouts', 'doh', fallback=5.0)
|
|
140
|
+
|
|
141
|
+
upstream_retries = config.getint('advanced', 'retries', fallback=2)
|
|
142
|
+
rate_limit_rps = config.getfloat('advanced', 'rate_limit_rps', fallback=0.0)
|
|
143
|
+
rate_limit_burst = config.getfloat('advanced', 'rate_limit_burst', fallback=0.0)
|
|
144
|
+
optimistic_cache_enabled = config.getboolean('advanced', 'optimistic_cache_enabled', fallback=False)
|
|
145
|
+
optimistic_stale_max_age = config.getint('advanced', 'optimistic_stale_max_age', fallback=86400)
|
|
146
|
+
optimistic_stale_response_ttl = config.getint('advanced', 'optimistic_stale_response_ttl', fallback=30)
|
|
147
|
+
pool_max_size = config.getint('advanced', 'pool_max_size', fallback=5)
|
|
148
|
+
pool_idle_timeout = config.getfloat('advanced', 'pool_idle_timeout', fallback=60.0)
|
|
149
|
+
doh_version = config.get('advanced', 'doh_version', fallback='auto').lower()
|
|
150
|
+
if doh_version not in ('auto', '1.1', '2', '3'):
|
|
151
|
+
doh_version = 'auto'
|
|
152
|
+
doh_auto_cache_ttl = config.getint('advanced', 'doh_auto_cache_ttl', fallback=3600)
|
|
153
|
+
|
|
154
|
+
dnssec_enabled = config.getboolean('security', 'dnssec_enabled', fallback=False)
|
|
155
|
+
auto_update_trust_anchor = config.getboolean('security', 'auto_update_trust_anchor', fallback=True)
|
|
156
|
+
trust_anchors_file = config.get('security', 'trust_anchors_file', fallback='')
|
|
157
|
+
|
|
158
|
+
pinned_raw = config.get('security', 'pinned_certs', fallback='')
|
|
159
|
+
dns_pinned_certs = {}
|
|
160
|
+
for item in [s.strip() for s in pinned_raw.split(',') if s.strip()]:
|
|
161
|
+
if '=' in item:
|
|
162
|
+
host, fp = item.split('=', 1)
|
|
163
|
+
dns_pinned_certs[host.strip()] = fp.strip()
|
|
164
|
+
|
|
165
|
+
dns_rebind_protection = config.getboolean('security', 'rebind_protection', fallback=False)
|
|
166
|
+
dns_rebind_action = config.get('security', 'rebind_action', fallback='strip').lower()
|
|
167
|
+
if dns_rebind_action not in ('strip', 'block'):
|
|
168
|
+
dns_rebind_action = 'strip'
|
|
169
|
+
|
|
170
|
+
dns_privilege_drop_user = config.get('security', 'dns_privilege_drop_user', fallback='')
|
|
171
|
+
dns_privilege_drop_group = config.get('security', 'dns_privilege_drop_group', fallback='')
|
|
172
|
+
dns_chroot_dir = config.get('security', 'dns_chroot_dir', fallback='')
|
|
173
|
+
|
|
174
|
+
dns_logging_enabled = config.getboolean('logging', 'enabled', fallback=False)
|
|
175
|
+
dns_log_dir = config.get('logging', 'log_dir', fallback=_default_log_dir())
|
|
176
|
+
dns_log_retention_days = config.getint('logging', 'retention_days', fallback=7)
|
|
177
|
+
dns_log_prefix = config.get('logging', 'log_prefix', fallback='dns-log')
|
|
178
|
+
|
|
179
|
+
metrics_enabled = config.getboolean('metrics', 'enabled', fallback=False)
|
|
180
|
+
metrics_port = config.getint('metrics', 'port', fallback=8000)
|
|
181
|
+
uvloop_enable = config.getboolean('metrics', 'uvloop_enable', fallback=False)
|
|
182
|
+
|
|
183
|
+
bootstrap_servers_raw = config.get('bootstrap', 'servers', fallback='')
|
|
184
|
+
if bootstrap_servers_raw.strip():
|
|
185
|
+
bootstrap_servers = [s.strip() for s in bootstrap_servers_raw.split(',') if s.strip()]
|
|
186
|
+
else:
|
|
187
|
+
bootstrap_servers = ['1.1.1.1:53', '8.8.8.8:53']
|
|
188
|
+
bootstrap_timeout = config.getfloat('bootstrap', 'timeout', fallback=2.0)
|
|
189
|
+
bootstrap_retries = config.getint('bootstrap', 'retries', fallback=2)
|
|
190
|
+
bootstrap = {
|
|
191
|
+
'servers': bootstrap_servers,
|
|
192
|
+
'timeout': bootstrap_timeout,
|
|
193
|
+
'retries': bootstrap_retries,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
upstreams = []
|
|
197
|
+
if config.has_section('upstreams') and config.has_option('upstreams', 'servers'):
|
|
198
|
+
server_names = [s.strip() for s in config.get('upstreams', 'servers').split(',') if s.strip()]
|
|
199
|
+
for name in server_names:
|
|
200
|
+
section = f'upstreams.{name}'
|
|
201
|
+
if not config.has_section(section):
|
|
202
|
+
continue
|
|
203
|
+
address = config.get(section, 'address', fallback='')
|
|
204
|
+
if not address:
|
|
205
|
+
continue
|
|
206
|
+
proto = config.get(section, 'protocol', fallback='udp').lower()
|
|
207
|
+
port_str = config.get(section, 'port', fallback=None)
|
|
208
|
+
if port_str:
|
|
209
|
+
try:
|
|
210
|
+
port = int(port_str)
|
|
211
|
+
except ValueError:
|
|
212
|
+
port = None
|
|
213
|
+
else:
|
|
214
|
+
default_ports = {'tls': 853, 'https': 443, 'quic': 853, 'udp': 53, 'tcp': 53}
|
|
215
|
+
port = default_ports.get(proto, 53)
|
|
216
|
+
hostname = config.get(section, 'hostname', fallback='') or address
|
|
217
|
+
path = config.get(section, 'path', fallback='')
|
|
218
|
+
us_doh_version = config.get(section, 'doh_version', fallback=doh_version).lower()
|
|
219
|
+
if us_doh_version not in ('auto', '1.1', '2', '3'):
|
|
220
|
+
us_doh_version = doh_version
|
|
221
|
+
upstreams.append({
|
|
222
|
+
'address': address,
|
|
223
|
+
'protocol': proto,
|
|
224
|
+
'port': port,
|
|
225
|
+
'hostname': hostname,
|
|
226
|
+
'doh_version': us_doh_version,
|
|
227
|
+
'path': path,
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
blocklists = {
|
|
231
|
+
'enabled': config.getboolean('blocklists', 'enabled', fallback=False),
|
|
232
|
+
'urls': [u.strip() for u in config.get('blocklists', 'urls', fallback='').split(',') if u.strip()],
|
|
233
|
+
'interval_seconds': config.getint('blocklists', 'interval_seconds', fallback=86400),
|
|
234
|
+
'action': config.get('blocklists', 'action', fallback='NXDOMAIN').upper(),
|
|
235
|
+
'local_blocklist_dir': config.get('blocklists', 'local_blocklist_dir', fallback='blocklists'),
|
|
236
|
+
'reload_on_change': config.getboolean('blocklists', 'reload_on_change', fallback=True),
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
validated_config = {
|
|
240
|
+
'listen_ip': listen_ip,
|
|
241
|
+
'listen_port': listen_port,
|
|
242
|
+
'upstream_dns': upstream_dns,
|
|
243
|
+
'protocol': protocol,
|
|
244
|
+
'dns_enable_dot': dns_enable_dot,
|
|
245
|
+
'dns_dot_port': dns_dot_port,
|
|
246
|
+
'dns_dot_cert_file': dns_dot_cert_file,
|
|
247
|
+
'dns_dot_key_file': dns_dot_key_file,
|
|
248
|
+
'dns_enable_doh': dns_enable_doh,
|
|
249
|
+
'dns_doh_port': dns_doh_port,
|
|
250
|
+
'dns_doh_cert_file': dns_doh_cert_file,
|
|
251
|
+
'dns_doh_key_file': dns_doh_key_file,
|
|
252
|
+
'dns_doh_path': dns_doh_path,
|
|
253
|
+
'verbose': verbose,
|
|
254
|
+
'disable_ipv6': disable_ipv6,
|
|
255
|
+
'strip_ipv6_records': strip_ipv6_records,
|
|
256
|
+
'dns_cache_ttl': dns_cache_ttl,
|
|
257
|
+
'dns_cache_max_size': dns_cache_max_size,
|
|
258
|
+
'dns_negative_cache_ttl': dns_negative_cache_ttl,
|
|
259
|
+
'dns_logging_enabled': dns_logging_enabled,
|
|
260
|
+
'dns_log_retention_days': dns_log_retention_days,
|
|
261
|
+
'dns_log_dir': dns_log_dir,
|
|
262
|
+
'dns_log_prefix': dns_log_prefix,
|
|
263
|
+
'dns_pinned_certs': dns_pinned_certs,
|
|
264
|
+
'dns_ecs_enabled': dns_ecs_enabled,
|
|
265
|
+
'dns_max_payload': dns_max_payload,
|
|
266
|
+
'dnssec_enabled': dnssec_enabled,
|
|
267
|
+
'auto_update_trust_anchor': auto_update_trust_anchor,
|
|
268
|
+
'metrics_enabled': metrics_enabled,
|
|
269
|
+
'metrics_port': metrics_port,
|
|
270
|
+
'uvloop_enable': uvloop_enable,
|
|
271
|
+
'upstream_retries': upstream_retries,
|
|
272
|
+
'upstream_udp_timeout': upstream_udp_timeout,
|
|
273
|
+
'upstream_tcp_timeout': upstream_tcp_timeout,
|
|
274
|
+
'upstream_doh_timeout': upstream_doh_timeout,
|
|
275
|
+
'rate_limit_rps': rate_limit_rps,
|
|
276
|
+
'rate_limit_burst': rate_limit_burst,
|
|
277
|
+
'upstreams': upstreams,
|
|
278
|
+
'optimistic_cache_enabled': optimistic_cache_enabled,
|
|
279
|
+
'optimistic_stale_max_age': optimistic_stale_max_age,
|
|
280
|
+
'optimistic_stale_response_ttl': optimistic_stale_response_ttl,
|
|
281
|
+
'dns_privilege_drop_user': dns_privilege_drop_user,
|
|
282
|
+
'dns_privilege_drop_group': dns_privilege_drop_group,
|
|
283
|
+
'dns_chroot_dir': dns_chroot_dir,
|
|
284
|
+
'dns_rebind_protection': dns_rebind_protection,
|
|
285
|
+
'dns_rebind_action': dns_rebind_action,
|
|
286
|
+
'pool_max_size': pool_max_size,
|
|
287
|
+
'pool_idle_timeout': pool_idle_timeout,
|
|
288
|
+
'doh_version': doh_version,
|
|
289
|
+
'doh_auto_cache_ttl': doh_auto_cache_ttl,
|
|
290
|
+
'bootstrap': bootstrap,
|
|
291
|
+
'blocklists': blocklists,
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
_validate_and_warn(validated_config)
|
|
295
|
+
return validated_config
|