blacklanternsecurity/blastdns
GitHub: blacklanternsecurity/blastdns
基于 Rust 的高性能 DNS 批量解析器,提供命令行、Rust 库和 Python 绑定三种使用方式,适合需要大规模快速 DNS 查询的场景。
Stars: 3 | Forks: 0
# BlastDNS
[](https://www.gnu.org/licenses/gpl-3.0)
[](https://www.rust-lang.org)
[](https://crates.io/crates/blastdns)
[](https://www.python.org/downloads/)
[](https://pypi.org/project/blastdns/)
[](https://github.com/blacklanternsecurity/blastdns/actions/workflows/rust-tests.yml)
[](https://github.com/blacklanternsecurity/blastdns/actions/workflows/python-tests.yml)
[BlastDNS](https://github.com/blacklanternsecurity/blastdns) 是一个用 Rust 编写的超快速 DNS 解析器。与 [massdns](https://github.com/blechschmidt/massdns) 类似,它的设计理念是你提供的解析器越多,运行速度就越快。其功能包括内置缓存,以及即使在处理不可靠的 DNS 服务器时也能保持高准确性。有关详细信息,请参阅[架构](#architecture)。BlastDNS 是 [BBOT](https://github.com/blacklanternsecurity/bbot) 使用的主要 DNS 库。
有以下三种使用方式:
- [Rust 命令行工具](#cli)
- [Rust 库](#rust-api)
- [Python 库](#python-api)
## 基准测试
使用 100 个 worker 对本地 `dnsmasq` 进行 10 万次 DNS 查询:
| 库 | 语言 | 时间 | QPS | 成功 | 失败 | 相比 dnspython |
|-----------------|--------|---------|--------|---------|------|---------------|
| massdns | C | 1.370s | 72,998 | 100,000 | 0 | 28.63x |
| blastdns-cli | Rust | 1.654s | 60,470 | 100,000 | 0 | 23.72x |
| blastdns-python | Python | 2.485s | 40,249 | 100,000 | 0 | 15.79x |
| dnspython | Python | 39.223s | 2,550 | 100,000 | 0 | 1.00x |
### 命令行工具
命令行工具使用指定的解析器列表对主机进行批量解析。它默认输出为 JSON 格式。
```
# 将所有结果发送至 jq
$ blastdns hosts.txt --rdtype A --resolvers resolvers.txt | jq
# 仅输出原始 IPv4 地址
$ blastdns hosts.txt --rdtype A --resolvers resolvers.txt | jq '.response.answers[].rdata.A'
# 从 stdin 加载
$ cat hosts.txt | blastdns --rdtype A --resolvers resolvers.txt
# 跳过空响应(例如,无应答的 NXDOMAIN)
$ blastdns hosts.txt --rdtype A --resolvers resolvers.txt --skip-empty | jq
# 跳过错误响应(例如,超时,连接失败)
$ blastdns hosts.txt --rdtype A --resolvers resolvers.txt --skip-errors | jq
```
#### 命令行帮助
```
$ blastdns --help
BlastDNS - Ultra-fast DNS Resolver written in Rust
Usage: blastdns [OPTIONS] --resolvers [HOSTS_TO_RESOLVE]
Arguments:
[HOSTS_TO_RESOLVE] File containing hostnames to resolve (one per line). Reads from stdin if not specified
Options:
--rdtype
Record type to query (A, AAAA, MX, ...) [default: A]
--resolvers
File containing DNS nameservers (one per line)
--threads-per-resolver
Worker threads per resolver [default: 2]
--timeout-ms
Per-request timeout in milliseconds [default: 1000]
--retries
Retry attempts after a resolver failure [default: 10]
--purgatory-threshold
Consecutive errors before a worker is put into timeout [default: 10]
--purgatory-sentence-ms
How many milliseconds a worker stays in timeout [default: 1000]
--skip-empty
Don't show responses with no answers
--skip-errors
Don't show error responses
--brief
Output brief format (hostname, record type, answers only)
--cache-capacity
DNS cache capacity (0 = disabled) [default: 10000]
-h, --help
Print help
-V, --version
Print version
```
#### JSON 输出示例
BlastDNS 默认输出为 JSON 格式:
```
{
"host": "microsoft.com",
"response": {
"additionals": [],
"answers": [
{
"dns_class": "IN",
"name_labels": "microsoft.com.",
"rdata": {
"A": "13.107.213.41"
},
"ttl": 1968
},
{
"dns_class": "IN",
"name_labels": "microsoft.com.",
"rdata": {
"A": "13.107.246.41"
},
"ttl": 1968
}
],
"edns": {
"flags": {
"dnssec_ok": false,
"z": 0
},
"max_payload": 1232,
"options": {
"options": []
},
"rcode_high": 0,
"version": 0
},
"header": {
"additional_count": 1,
"answer_count": 2,
"authentic_data": false,
"authoritative": false,
"checking_disabled": false,
"id": 62150,
"message_type": "Response",
"name_server_count": 0,
"op_code": "Query",
"query_count": 1,
"recursion_available": true,
"recursion_desired": true,
"response_code": "NoError",
"truncation": false
},
"name_servers": [],
"queries": [
{
"name": "microsoft.com.",
"query_class": "IN",
"query_type": "A"
}
],
"signature": []
}
}
```
#### 调试日志
BlastDNS 使用标准的 Rust `tracing` 生态系统。通过设置 `RUST_LOG` 环境变量来启用调试日志:
```
# 仅显示来自 blastdns 的 debug 日志
RUST_LOG=blastdns=debug blastdns hosts.txt --rdtype A --resolvers resolvers.txt
# 显示所有的 debug 日志
RUST_LOG=debug blastdns hosts.txt --rdtype A --resolvers resolvers.txt
# 显示 trace 级别日志以了解详细的内部行为
RUST_LOG=blastdns=trace blastdns hosts.txt --rdtype A --resolvers resolvers.txt
```
有效的日志级别(从最低到最高详细程度):`error`、`warn`、`info`、`debug`、`trace`
### Rust API
#### 安装
```
# 安装 CLI 工具
cargo install blastdns
# 将库添加到您的项目
cargo add blastdns
```
#### 用法
BlastDNS 既可以使用系统解析器(从操作系统配置中自动检测),也可以使用自定义解析器:
```
use blastdns::{BlastDNSClient, BlastDNSConfig};
use futures::StreamExt;
use hickory_client::proto::rr::RecordType;
use std::time::Duration;
// Option 1: Use system DNS resolvers (default)
let client = BlastDNSClient::new(vec![]).await?;
// Check what resolvers are being used
println!("Using resolvers: {:?}", client.resolvers());
// Option 2: Read DNS resolvers from a file (one per line -> vector of strings)
let resolvers = std::fs::read_to_string("resolvers.txt")
.expect("Failed to read resolvers file")
.lines()
.map(str::to_string)
.collect::>();
// create a new blastdns client with default config
let client = BlastDNSClient::new(resolvers).await?;
// or with custom config
let mut config = BlastDNSConfig::default();
config.threads_per_resolver = 5;
config.request_timeout = Duration::from_secs(2);
let client = BlastDNSClient::with_config(resolvers, config).await?;
// resolve: lookup a domain, returns only the rdata strings
let answers = client.resolve("example.com", RecordType::A).await?;
for answer in answers {
println!("{}", answer); // e.g., "93.184.216.34"
}
// resolve_full: lookup a domain, returns the full DNS response
let result = client.resolve_full("example.com", RecordType::A).await?;
println!("{}", serde_json::to_string_pretty(&result).unwrap());
// resolve_batch: process many hosts in parallel, returns simplified output
// streams back (host, record_type, Vec) tuples as they complete
// automatically filters out errors and empty responses
let wordlist = ["one.example", "two.example", "three.example"];
let mut stream = client.resolve_batch(
wordlist.into_iter().map(Ok::<_, std::convert::Infallible>),
RecordType::A,
);
while let Some((host, record_type, answers)) = stream.next().await {
println!("{} ({}):", host, record_type);
for answer in answers {
println!(" {}", answer); // e.g., "93.184.216.34" for A records
}
}
// resolve_batch_full: process many hosts with full DNS response structures
// streams back (host, Result) tuples with configurable filtering
let wordlist = ["one.example", "two.example", "three.example"];
let mut stream = client.resolve_batch_full(
wordlist.into_iter().map(Ok::<_, std::convert::Infallible>),
RecordType::A,
false, // skip_empty: don't filter out empty responses
false, // skip_errors: don't filter out errors
);
while let Some((host, outcome)) = stream.next().await {
match outcome {
Ok(response) => println!("{}: {} answers", host, response.answers().len()),
Err(err) => eprintln!("{} failed: {err}", host),
}
}
// resolve_multi: resolve multiple record types for a single host
// returns only successful results with answers as dict[record_type, Vec]
let record_types = vec![RecordType::A, RecordType::AAAA, RecordType::MX];
let results = client.resolve_multi("example.com", record_types).await?;
for (record_type, answers) in results {
println!("{}: {} answers", record_type, answers.len());
for answer in answers {
println!(" {}", answer);
}
}
// resolve_multi_full: resolve multiple record types with full responses
// returns all results (success and failure) as dict[record_type, Result]
let record_types = vec![RecordType::A, RecordType::AAAA, RecordType::MX];
let results = client.resolve_multi_full("example.com", record_types).await?;
for (record_type, result) in results {
match result {
Ok(response) => println!("{}: {} answers", record_type, response.answers().len()),
Err(err) => eprintln!("{} failed: {err}", record_type),
}
}
```
#### 系统解析器
您可以通过编程方式获取系统配置的 DNS 解析器:
```
use blastdns::get_system_resolvers;
// Get system resolver IPs (works on Unix, Windows, macOS, Android)
let resolver_ips = get_system_resolvers()?;
for ip in resolver_ips {
println!("System resolver: {}", ip);
}
```
#### 用于测试的 MockBlastDNSClient
`MockBlastDNSClient` 实现了 `DnsResolver` trait,并提供了一个直接替换方案,它可以在不发出真实网络请求的情况下返回虚构的 DNS 响应。
```
use blastdns::{MockBlastDNSClient, DnsResolver};
use hickory_client::proto::rr::RecordType;
use std::collections::HashMap;
// Create a mock client
let mut mock_client = MockBlastDNSClient::new();
// Configure mock responses
let responses = HashMap::from([
(
"example.com".to_string(),
HashMap::from([
("A".to_string(), vec!["93.184.216.34".to_string()]),
("AAAA".to_string(), vec!["2606:2800:220:1:248:1893:25c8:1946".to_string()]),
]),
),
]);
// Hosts that should return NXDOMAIN
let nxdomains = vec!["notfound.example.com".to_string()];
mock_client.mock_dns(responses, nxdomains);
// Use like any DnsResolver
let answers = mock_client.resolve("example.com".to_string(), RecordType::A).await?;
assert_eq!(answers, vec!["93.184.216.34"]);
// NXDOMAIN hosts return empty responses
let answers = mock_client.resolve("notfound.example.com".to_string(), RecordType::A).await?;
assert_eq!(answers.len(), 0);
```
`MockBlastDNSClient` 支持所有的 `DnsResolver` 方法,包括 `resolve`、`resolve_full`、`resolve_batch`、`resolve_batch_full`、`resolve_multi` 和 `resolve_multi_full`。
### Python API
`blastdns` Python 包是对 Rust 库的一个轻量级封装。
#### 安装
```
# 使用 pip
pip install blastdns
# 使用 uv
uv add blastdns
# 使用 poetry
poetry add blastdns
```
#### 开发环境设置
```
# 安装 Python 依赖
uv sync
# 构建并安装 Rust->Python bindings
uv run maturin develop
# 运行测试
uv run pytest
```
#### 用法
要在 Python 中使用它,您可以使用 `Client` 类:
```
import asyncio
from blastdns import Client, ClientConfig, DNSResult, DNSError, get_system_resolvers
async def main():
# Option 1: Use system resolvers (pass empty list)
client = Client([], ClientConfig(threads_per_resolver=4, request_timeout_ms=1500))
# Check what resolvers are being used
print(f"Using resolvers: {client.resolvers}")
# Option 2: Manually get system resolvers
system_resolvers = get_system_resolvers()
print(f"System resolvers: {system_resolvers}")
# Option 3: Use custom resolvers
resolvers = ["1.1.1.1:53", "8.8.8.8:53"]
client = Client(resolvers, ClientConfig(threads_per_resolver=4, request_timeout_ms=1500))
# resolve: lookup a single host, returns only rdata strings
answers = await client.resolve("example.com", "A")
for answer in answers:
print(f" {answer}") # e.g., "93.184.216.34"
# resolve_full: lookup a single host, returns full DNS response as Pydantic model
result = await client.resolve_full("example.com", "AAAA")
print(f"Host: {result.host}")
print(f"Response code: {result.response.header.response_code}")
for answer in result.response.answers:
print(f" {answer.name_labels}: {answer.rdata}")
# resolve_batch: simplified batch resolution with minimal output
# returns only (host, record_type, list[rdata]) - no full DNS response structures
# automatically filters out errors and empty responses
hosts = ["example.com", "google.com", "github.com"]
async for host, rdtype, answers in client.resolve_batch(hosts, "A"):
print(f"{host} ({rdtype}):")
for answer in answers:
print(f" {answer}") # e.g., "93.184.216.34" for A records
# resolve_batch_full: process many hosts in parallel with full responses
# streams results back as they complete
hosts = ["one.example.com", "two.example.com", "three.example.com"]
async for host, result in client.resolve_batch_full(hosts, "A"):
if isinstance(result, DNSError):
print(f"{host} failed: {result.error}")
else:
print(f"{host}: {len(result.response.answers)} answers")
# resolve_multi: resolve multiple record types for a single host in parallel
# returns only successful results with answers
record_types = ["A", "AAAA", "MX"]
results = await client.resolve_multi("example.com", record_types)
for record_type, answers in results.items():
print(f"{record_type}: {answers}")
# resolve_multi_full: resolve multiple record types with full response data
record_types = ["A", "AAAA", "MX"]
results = await client.resolve_multi_full("example.com", record_types)
for record_type, result in results.items():
if isinstance(result, DNSError):
print(f"{record_type} failed: {result.error}")
else:
print(f"{record_type}: {len(result.response.answers)} answers")
asyncio.run(main())
```
#### Python API 方法
- **`Client.resolvers`** (属性):获取此客户端正在使用的解析器地址列表。返回一个字符串列表(例如,`["8.8.8.8:53", "1.1.1.1:53"]`)。
- **`get_system_resolvers() -> list[str]`**:从操作系统配置中获取系统 DNS 解析器的 IP 地址。支持 Unix、Windows、macOS 和 Android。返回不带端口的 IP 地址列表(例如,`["8.8.8.8", "1.1.1.1"]`)。适用于检查操作系统配置使用的解析器。
- **`Client.resolve(host, record_type=None) -> list[str]`**:查询单个主机名,仅返回 rdata 字符串。默认为 `A` 记录。返回一个字符串列表(例如,对于 A 记录返回 `["93.184.216.34"]`)。适用于只需要记录数据而不需要完整 DNS 响应结构的简单用例。
- **`Client.resolve_full(host, record_type=None) -> DNSResult`**:查询单个主机名,返回完整的 DNS 响应。默认为 `A` 记录。返回一个带有类型化字段的 Pydantic `DNSResult` 模型,以便于访问 header、queries、answers 等内容。
- **`Client.resolve_batch(hosts, record_type=None)`**:简化的批量解析,仅返回关键数据。接受一个主机名的可迭代对象,并以流的形式返回 `(host, record_type, answers)` 元组,其中 `answers` 是一个 rdata 字符串列表(例如,对于 A 记录为 `["93.184.216.34"]`,对于 MX 记录为 `["10 aspmx.l.google.com."]`)。自动过滤掉错误和空响应。非常适合高效处理大型主机列表。
- **`Client.resolve_batch_full(hosts, record_type=None, skip_empty=False, skip_errors=False)`**:通过完整的 DNS 响应并行解析多个主机。接受一个主机名的可迭代对象,并在结果完成时以流的形式返回 `(host, result)` 元组。每个结果都是一个 `DNSResult` 或 `DNSError` Pydantic 模型。设置 `skip_empty=True` 以过滤掉没有答案的成功响应。设置 `skip_errors=True` 以过滤掉错误响应。
- **`Client.resolve_multi(host, record_types) -> dict[str, list[str]]`**:并行解析单个主机的多种记录类型,仅返回带有答案的成功结果。接受一个记录类型字符串列表(例如,`["A", "AAAA", "MX"]`),并返回一个将记录类型映射到 rdata 字符串列表的字典。仅包含解析成功且有答案的记录类型。
- **`Client.resolve_multi_full(host, record_types) -> dict[str, DNSResultOrError]`**:并行解析单个主机的多种记录类型,返回完整的 DNS 响应。接受一个记录类型字符串列表,并返回一个以记录类型为键的字典。每个值要么是 `DNSResult`(成功),要么是 `DNSError`(失败)Pydantic 模型。包含所有记录类型,即使是失败或没有答案的类型。
#### 用于测试的 MockClient
`MockClient` 提供了一个可直接替换 `Client` 的方案,它可以在不发出真实网络请求的情况下返回虚构的 DNS 响应。它实现了与 `Client` 相同的接口,非常适合用于测试依赖 DNS 查询的代码。
```
import pytest
from blastdns import MockClient, DNSResult
@pytest.fixture
def mock_client():
"""Create a mock client with pre-configured test data."""
client = MockClient()
client.mock_dns({
"example.com": {
"A": ["93.184.216.34"],
"AAAA": ["2606:2800:220:1:248:1893:25c8:1946"],
"MX": ["10 aspmx.l.google.com.", "20 alt1.aspmx.l.google.com."],
},
"cname.example.com": {
"CNAME": ["example.com."]
},
"_NXDOMAIN": ["notfound.example.com"], # hosts that return NXDOMAIN
})
return client
@pytest.mark.asyncio
async def test_my_function(mock_client):
# resolve() returns simple rdata strings
answers = await mock_client.resolve("example.com", "A")
assert answers == ["93.184.216.34"]
# resolve_full() returns full DNS response structure
result = await mock_client.resolve_full("example.com", "A")
assert isinstance(result, DNSResult)
assert len(result.response.answers) == 1
# NXDOMAIN hosts return empty responses (not errors)
answers = await mock_client.resolve("notfound.example.com", "A")
assert len(answers) == 0
# resolve_batch() works with all mocked hosts
async for host, rdtype, answers in mock_client.resolve_batch(["example.com"], "A"):
print(f"{host}: {answers}") # ["93.184.216.34"]
# resolve_multi() resolves multiple record types in parallel
results = await mock_client.resolve_multi("example.com", ["A", "AAAA", "MX"])
assert len(results) == 3
assert results["MX"] == ["10 aspmx.l.google.com.", "20 alt1.aspmx.l.google.com."]
```
**正则表达式模式:**
带有 `regex:` 前缀的主机名会被视为正则表达式模式,从而支持通配符和动态匹配:
```
client = MockClient()
client.mock_dns({
# Exact match
"specific.example.com": {"A": ["10.0.0.1"]},
# Regex: match any subdomain of example.com
"regex:.*\\.example\\.com": {"A": ["192.168.1.1"]},
# Regex: match numbered servers
"regex:^server-\\d+\\.test\\.com$": {"A": ["10.0.0.1"]},
# Regex patterns work for NXDOMAIN too
"_NXDOMAIN": ["regex:^bad-.*\\.example\\.com$"],
})
```
精确匹配的优先级高于正则表达式模式。当多个正则表达式模式匹配时,第一个匹配的胜出。
**主要特性:**
- 支持所有的 `Client` 方法:`resolve`、`resolve_full`、`resolve_batch`、`resolve_batch_full`、`resolve_multi`、`resolve_multi_full`
- 返回与 `Client` 相同的数据结构,实现直接替换的兼容性
- NXDOMAIN 主机(在 `_NXDOMAIN` 列表中指定)返回带有 `NXDomain` 响应码的响应
- 未被 Mock 的主机返回空响应
- 像真实的客户端一样自动格式化 PTR 查询(IP 地址 → 反向 DNS 格式)
- 带有 `regex:` 前缀的主机名用于通配符/模式匹配
#### 异常
所有由 blastdns 抛出的错误都是 `BlastDNSError` 的子类:
```
BlastDNSError
├── ConfigurationError # invalid resolver address, invalid hostname, bad config
│ └── NoResolversError # no resolvers provided or detected
└── ResolverError # resolver failed (timeout, connection failure, etc.)
```
```
from blastdns import Client, BlastDNSError, ConfigurationError, NoResolversError, ResolverError
# 广泛捕获
try:
client = Client(["not-an-ip"])
except BlastDNSError as e:
print(f"blastdns error: {e}")
# 精准捕获
try:
client = Client(["not-an-ip"])
except ConfigurationError as e:
print(f"bad config: {e}")
# 在查询期间捕获解析器故障
try:
result = await client.resolve_full("example.com", "A")
except ResolverError as e:
print(f"resolver failed: {e}")
```
#### 响应模型
`*_full()` 方法返回 Pydantic V2 模型,以提供类型安全并在 IDE 中实现自动补全:
- **`DNSResult`**:成功的 DNS 响应,包含 `host` 和 `response` 字段
- **`DNSError`**:失败的 DNS 查询,包含一个 `error` 字段
- **`Response`**:DNS 消息,包含 `header`、`queries`、`answers`、`name_servers` 等
基础方法(`resolve`、`resolve_batch`、`resolve_multi`)返回简单的 Python 类型(列表、字典、字符串),以便在不需要完整响应结构时提供便利。
`ClientConfig` 暴露了上面展示的配置项(`threads_per_resolver`、`request_timeout_ms`、`max_retries`、`purgatory_threshold`、`purgatory_sentence_ms`),并在将它们传递给 Rust 核心之前进行验证。
## 架构
BlastDNS 构建在 [`hickory-dns`](https://github.com/hickory-dns/hickory-dns) 之上,但仅使用了其底层的 Client API,而没有使用 Resolver API。
在 `BlastDNSClient` 的底层,每个解析器都会获得属于自己的 `ResolverWorker` 任务,并且每个解析器可配置的 worker 数量(默认:2,可通过 `BlastDNSConfig.threads_per_resolver` 进行配置)。
当用户调用 `BlastDNSClient::resolve` 时,会创建一个新的 `WorkItem`,其中包含请求(主机 + rdtype)和一个用于保存结果的 oneshot channel。这个 `WorkItem` 被放入 [crossfire](https://github.com/frostyplanet/crossfire-rs) MPMC 队列中,等待第一个可用的 `ResolverWorker` 捡取。Worker 会在发出第一个请求时被惰性生成。
### 缓存
BlastDNS 包含一个可选的、具备 TTL 感知能力的缓存,该缓存采用 LRU(最近最少使用)淘汰策略。该缓存默认启用,容量为 10,000 个条目,可以进行配置或完全禁用:
- 仅缓存**带有答案的肯定响应**(不缓存错误、NXDOMAIN 或空响应)
- 缓存条目会根据 DNS 记录的 TTL 自动过期(TTL 会被限制在可配置的最小/最大范围内)
- 过期的条目在被访问时会被移除;未被访问的过期条目将保留,直到被 LRU 策略淘汰
- 缓存具有硬性容量限制(即使存在过期条目也能防止无限增长)
- 线程安全,具有最小的锁争用
通过 `BlastDNSConfig` 进行配置:
- `cache_capacity`:条目数量(默认:10000,设置为 0 以禁用)
- `cache_min_ttl`:最小 TTL(默认:10 秒)
- `cache_max_ttl`:最大 TTL(默认:1 天)
### 重试逻辑与容错机制
BlastDNS 通过多层重试系统来处理不可靠的解析器:
**客户端级别重试**:当查询失败并出现可重试错误(网络超时、连接失败)时,客户端会自动重试最多 `max_retries` 次(默认:10 次)。每次重试都会创建一个全新的 `WorkItem` 并将其发送回共享队列,在那里它可以被**任何可用的 worker** 捡取——而不必是同一个解析器。这意味着重试会自然地绕过有问题的解析器。
**Purgatory(“炼狱”惩罚)系统**:每个 worker 都会跟踪连续错误。在达到 `purgatory_threshold` 次失败(默认:10 次)后,该 worker 将进入“purgatory”状态——它会休眠 `purgatory_sentence` 毫秒(默认:1000 毫秒),然后再恢复工作。这会在不将它们完全移除的情况下,暂时将那些挣扎的解析器边缘化,如果解析器的问题是短暂的,系统可以进行自我修复。
**不可重试的错误**:配置错误(无效的主机名)和系统错误(队列关闭)会立即失败,不予重试,从而防止在注定无法成功的查询上浪费资源。
这种架构确保了即使在由可靠和不可靠的 DNS 服务器混合组成的池中也能实现最大的准确性,因为查询会自然地迁移到响应迅速的解析器,而那些有问题的解析器则会自动进行自我节流。
## 测试
BlastDNS 包含两种类型的测试:
### 单元测试(无需 DNS 服务器)
单元测试使用 `MockBlastDNSClient (Rust) 或 `MockClient` (Python) 运行,不需要任何外部依赖:
```
# Rust 单元测试
cargo test
# Python 单元测试
uv run pytest
```
### 集成测试(需要 DNS 服务器)
集成测试针对运行在 `127.0.0.1:5353` 和 `[::1]:5353` 上的本地 `dnsmasq` 服务器验证真实的 DNS 解析。
安装 `dnsmasq`:
```
sudo apt install dnsmasq
```
启动测试 DNS 服务器:
```
sudo ./scripts/start-test-dns.sh
```
运行集成测试:
```
# Rust 集成测试(标记为 #[ignore])
cargo test -- --ignored
# 使用真实 DNS 的 Python 集成测试
uv run pytest -k "not mock"
```
测试完成后,停止测试 DNS 服务器:
```
./scripts/stop-test-dns.sh
```
## 代码检查
### Rust
```
# 运行 clippy 进行 lint 检查
cargo clippy --all-targets --all-features
# 运行 rustfmt 进行格式化
cargo fmt --all
```
### Python
```
# 运行 ruff 进行 lint 检查
uv run ruff check --fix
# 运行 ruff 进行格式化
uv run ruff format
```
标签:BBOT, DNS枚举, DNS解析器, GitHub, GPLv3, Python 3.9+, Python绑定, Rust, Rust库, SEO优化, 可视化界面, 子域名爆破, 无类别, 无线安全, 缓存, 网络安全, 网络安全工具, 网络流量审计, 跨语言调用, 逆向工具, 通知系统, 隐私保护, 高速DNS查询