blacklanternsecurity/blastdns

GitHub: blacklanternsecurity/blastdns

基于 Rust 的高性能 DNS 批量解析器,提供命令行、Rust 库和 Python 绑定三种使用方式,适合需要大规模快速 DNS 查询的场景。

Stars: 3 | Forks: 0

# BlastDNS [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-black.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Rust 2024](https://img.shields.io/badge/rust-2024-orange.svg)](https://www.rust-lang.org) [![Crates.io](https://img.shields.io/crates/v/blastdns.svg?color=orange)](https://crates.io/crates/blastdns) [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/) [![PyPI version](https://img.shields.io/pypi/v/blastdns.svg?color=blue)](https://pypi.org/project/blastdns/) [![Rust Tests](https://static.pigsec.cn/wp-content/uploads/repos/2026/04/9f577bc227113927.svg)](https://github.com/blacklanternsecurity/blastdns/actions/workflows/rust-tests.yml) [![Python Tests](https://static.pigsec.cn/wp-content/uploads/repos/2026/04/7b709adc08113928.svg)](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查询