{"id":33922361,"url":"https://github.com/blacklanternsecurity/blastdns","last_synced_at":"2025-12-12T09:02:04.177Z","repository":{"id":327610580,"uuid":"1101551857","full_name":"blacklanternsecurity/blastdns","owner":"blacklanternsecurity","description":"An ultra-fast DNS resolver written in Rust, with Python bindings","archived":false,"fork":false,"pushed_at":"2025-12-08T00:31:03.000Z","size":567,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"stable","last_synced_at":"2025-12-08T00:39:48.146Z","etag":null,"topics":["cli","dns","dns-resolver","osint","python","python-bindings","rust"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/blacklanternsecurity.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-11-21T20:57:03.000Z","updated_at":"2025-12-06T03:17:18.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/blacklanternsecurity/blastdns","commit_stats":null,"previous_names":["blacklanternsecurity/blastdns"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/blacklanternsecurity/blastdns","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blacklanternsecurity%2Fblastdns","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blacklanternsecurity%2Fblastdns/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blacklanternsecurity%2Fblastdns/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blacklanternsecurity%2Fblastdns/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/blacklanternsecurity","download_url":"https://codeload.github.com/blacklanternsecurity/blastdns/tar.gz/refs/heads/stable","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blacklanternsecurity%2Fblastdns/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":27679798,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-12-12T02:00:06.775Z","response_time":129,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","dns","dns-resolver","osint","python","python-bindings","rust"],"created_at":"2025-12-12T09:01:08.067Z","updated_at":"2025-12-12T09:02:04.165Z","avatar_url":"https://github.com/blacklanternsecurity.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# BlastDNS\n\n[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-black.svg)](https://www.gnu.org/licenses/gpl-3.0)\n[![Rust 2024](https://img.shields.io/badge/rust-2024-orange.svg)](https://www.rust-lang.org)\n[![Crates.io](https://img.shields.io/crates/v/blastdns.svg?color=orange)](https://crates.io/crates/blastdns)\n[![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)\n[![PyPI version](https://img.shields.io/pypi/v/blastdns.svg?color=blue)](https://pypi.org/project/blastdns/)\n[![Rust Tests](https://github.com/blacklanternsecurity/blastdns/actions/workflows/rust-tests.yml/badge.svg)](https://github.com/blacklanternsecurity/blastdns/actions/workflows/rust-tests.yml)\n[![Python Tests](https://github.com/blacklanternsecurity/blastdns/actions/workflows/python-tests.yml/badge.svg)](https://github.com/blacklanternsecurity/blastdns/actions/workflows/python-tests.yml)\n\n[BlastDNS](https://github.com/blacklanternsecurity/blastdns) is an ultra-fast DNS resolver written in Rust. Like [massdns](https://github.com/blechschmidt/massdns), it's designed to be faster the more resolvers you give it. Features include built-in caching, and high accuracy even with unreliable DNS servers. For details, see [Architecture](#architecture). BlastDNS is the main DNS library used by [BBOT](https://github.com/blacklanternsecurity/bbot).\n\nThere are three ways to use it:\n\n- [Rust CLI tool](#cli)\n- [Rust library](#rust-api)\n- [Python library](#python-api)\n\n## Benchmark\n\n100K DNS lookups against local `dnsmasq`, with 100 workers:\n\n| Library         | Language | Time    | QPS    | Success | Failed | vs dnspython |\n|-----------------|----------|---------|--------|---------|--------|--------------|\n| massdns         | C        | 1.370s  | 72,998 | 100,000 | 0      | 28.63x       |\n| blastdns-cli    | Rust     | 1.654s  | 60,470 | 100,000 | 0      | 23.72x       |\n| blastdns-python | Python   | 2.485s  | 40,249 | 100,000 | 0      | 15.79x       |\n| dnspython       | Python   | 39.223s | 2,550  | 100,000 | 0      | 1.00x        |\n\n### CLI\n\nThe CLI mass-resolves hosts using a specified list of resolvers. It outputs to JSON.\n\n```bash\n# send all results to jq\n$ blastdns hosts.txt --rdtype A --resolvers resolvers.txt | jq\n\n# print only the raw IPv4 addresses\n$ blastdns hosts.txt --rdtype A --resolvers resolvers.txt | jq '.response.answers[].rdata.A'\n\n# load from stdin\n$ cat hosts.txt | blastdns --rdtype A --resolvers resolvers.txt\n\n# skip empty responses (e.g., NXDOMAIN with no answers)\n$ blastdns hosts.txt --rdtype A --resolvers resolvers.txt --skip-empty | jq\n\n# skip error responses (e.g., timeouts, connection failures)\n$ blastdns hosts.txt --rdtype A --resolvers resolvers.txt --skip-errors | jq\n```\n\n#### CLI Help\n\n```\n$ blastdns --help\nBlastDNS - Ultra-fast DNS Resolver written in Rust\n\nUsage: blastdns [OPTIONS] --resolvers \u003cFILE\u003e [HOSTS_TO_RESOLVE]\n\nArguments:\n  [HOSTS_TO_RESOLVE]  File containing hostnames to resolve (one per line). Reads from stdin if not specified\n\nOptions:\n      --rdtype \u003cRECORD_TYPE\u003e\n          Record type to query (A, AAAA, MX, ...) [default: A]\n      --resolvers \u003cFILE\u003e\n          File containing DNS nameservers (one per line)\n      --threads-per-resolver \u003cTHREADS_PER_RESOLVER\u003e\n          Worker threads per resolver [default: 2]\n      --timeout-ms \u003cTIMEOUT_MS\u003e\n          Per-request timeout in milliseconds [default: 1000]\n      --retries \u003cRETRIES\u003e\n          Retry attempts after a resolver failure [default: 10]\n      --purgatory-threshold \u003cPURGATORY_THRESHOLD\u003e\n          Consecutive errors before a worker is put into timeout [default: 10]\n      --purgatory-sentence-ms \u003cPURGATORY_SENTENCE_MS\u003e\n          How many milliseconds a worker stays in timeout [default: 1000]\n      --skip-empty\n          Don't show responses with no answers\n      --skip-errors\n          Don't show error responses\n      --brief\n          Output brief format (hostname, record type, answers only)\n      --cache-capacity \u003cCACHE_CAPACITY\u003e\n          DNS cache capacity (0 = disabled) [default: 10000]\n  -h, --help\n          Print help\n  -V, --version\n          Print version\n```\n\n#### Example JSON output\n\nBlastDNS outputs to JSON by default:\n\n```json\n{\n  \"host\": \"microsoft.com\",\n  \"response\": {\n    \"additionals\": [],\n    \"answers\": [\n      {\n        \"dns_class\": \"IN\",\n        \"name_labels\": \"microsoft.com.\",\n        \"rdata\": {\n          \"A\": \"13.107.213.41\"\n        },\n        \"ttl\": 1968\n      },\n      {\n        \"dns_class\": \"IN\",\n        \"name_labels\": \"microsoft.com.\",\n        \"rdata\": {\n          \"A\": \"13.107.246.41\"\n        },\n        \"ttl\": 1968\n      }\n    ],\n    \"edns\": {\n      \"flags\": {\n        \"dnssec_ok\": false,\n        \"z\": 0\n      },\n      \"max_payload\": 1232,\n      \"options\": {\n        \"options\": []\n      },\n      \"rcode_high\": 0,\n      \"version\": 0\n    },\n    \"header\": {\n      \"additional_count\": 1,\n      \"answer_count\": 2,\n      \"authentic_data\": false,\n      \"authoritative\": false,\n      \"checking_disabled\": false,\n      \"id\": 62150,\n      \"message_type\": \"Response\",\n      \"name_server_count\": 0,\n      \"op_code\": \"Query\",\n      \"query_count\": 1,\n      \"recursion_available\": true,\n      \"recursion_desired\": true,\n      \"response_code\": \"NoError\",\n      \"truncation\": false\n    },\n    \"name_servers\": [],\n    \"queries\": [\n      {\n        \"name\": \"microsoft.com.\",\n        \"query_class\": \"IN\",\n        \"query_type\": \"A\"\n      }\n    ],\n    \"signature\": []\n  }\n}\n```\n\n#### Debug Logging\n\nBlastDNS uses the standard Rust `tracing` ecosystem. Enable debug logging by setting the `RUST_LOG` environment variable:\n\n```bash\n# Show debug logs from blastdns only\nRUST_LOG=blastdns=debug blastdns hosts.txt --rdtype A --resolvers resolvers.txt\n\n# Show debug logs from everything\nRUST_LOG=debug blastdns hosts.txt --rdtype A --resolvers resolvers.txt\n\n# Show trace-level logs for detailed internal behavior\nRUST_LOG=blastdns=trace blastdns hosts.txt --rdtype A --resolvers resolvers.txt\n```\n\nValid log levels (from least to most verbose): `error`, `warn`, `info`, `debug`, `trace`\n\n### Rust API\n\n#### Installation\n\n```bash\n# Install CLI tool\ncargo install blastdns\n\n# Add library to your project\ncargo add blastdns\n```\n\n#### Usage\n\n```rust\nuse blastdns::{BlastDNSClient, BlastDNSConfig};\nuse futures::StreamExt;\nuse hickory_client::proto::rr::RecordType;\nuse std::time::Duration;\n\n// read DNS resolvers from a file (one per line -\u003e vector of strings)\nlet resolvers = std::fs::read_to_string(\"resolvers.txt\")\n    .expect(\"Failed to read resolvers file\")\n    .lines()\n    .map(str::to_string)\n    .collect::\u003cVec\u003cString\u003e\u003e();\n\n// create a new blastdns client with default config\nlet client = BlastDNSClient::new(resolvers).await?;\n\n// or with custom config\nlet mut config = BlastDNSConfig::default();\nconfig.threads_per_resolver = 5;\nconfig.request_timeout = Duration::from_secs(2);\nlet client = BlastDNSClient::with_config(resolvers, config).await?;\n\n// resolve: lookup a domain, returns only the rdata strings\nlet answers = client.resolve(\"example.com\", RecordType::A).await?;\nfor answer in answers {\n    println!(\"{}\", answer);  // e.g., \"93.184.216.34\"\n}\n\n// resolve_full: lookup a domain, returns the full DNS response\nlet result = client.resolve_full(\"example.com\", RecordType::A).await?;\nprintln!(\"{}\", serde_json::to_string_pretty(\u0026result).unwrap());\n\n// resolve_batch: process many hosts in parallel, returns simplified output\n// streams back (host, record_type, Vec\u003crdata\u003e) tuples as they complete\n// automatically filters out errors and empty responses\nlet wordlist = [\"one.example\", \"two.example\", \"three.example\"];\nlet mut stream = client.resolve_batch(\n    wordlist.into_iter().map(Ok::\u003c_, std::convert::Infallible\u003e),\n    RecordType::A,\n);\nwhile let Some((host, record_type, answers)) = stream.next().await {\n    println!(\"{} ({}):\", host, record_type);\n    for answer in answers {\n        println!(\"  {}\", answer);  // e.g., \"93.184.216.34\" for A records\n    }\n}\n\n// resolve_batch_full: process many hosts with full DNS response structures\n// streams back (host, Result\u003cresponse\u003e) tuples with configurable filtering\nlet wordlist = [\"one.example\", \"two.example\", \"three.example\"];\nlet mut stream = client.resolve_batch_full(\n    wordlist.into_iter().map(Ok::\u003c_, std::convert::Infallible\u003e),\n    RecordType::A,\n    false,  // skip_empty: don't filter out empty responses\n    false,  // skip_errors: don't filter out errors\n);\nwhile let Some((host, outcome)) = stream.next().await {\n    match outcome {\n        Ok(response) =\u003e println!(\"{}: {} answers\", host, response.answers().len()),\n        Err(err) =\u003e eprintln!(\"{} failed: {err}\", host),\n    }\n}\n\n// resolve_multi: resolve multiple record types for a single host\n// returns only successful results with answers as dict[record_type, Vec\u003crdata\u003e]\nlet record_types = vec![RecordType::A, RecordType::AAAA, RecordType::MX];\nlet results = client.resolve_multi(\"example.com\", record_types).await?;\nfor (record_type, answers) in results {\n    println!(\"{}: {} answers\", record_type, answers.len());\n    for answer in answers {\n        println!(\"  {}\", answer);\n    }\n}\n\n// resolve_multi_full: resolve multiple record types with full responses\n// returns all results (success and failure) as dict[record_type, Result\u003cresponse\u003e]\nlet record_types = vec![RecordType::A, RecordType::AAAA, RecordType::MX];\nlet results = client.resolve_multi_full(\"example.com\", record_types).await?;\nfor (record_type, result) in results {\n    match result {\n        Ok(response) =\u003e println!(\"{}: {} answers\", record_type, response.answers().len()),\n        Err(err) =\u003e eprintln!(\"{} failed: {err}\", record_type),\n    }\n}\n```\n\n#### MockBlastDNSClient for Testing\n\n`MockBlastDNSClient` implements the `DnsResolver` trait and provides a drop-in replacement that returns fabricated DNS responses without making real network requests.\n\n```rust\nuse blastdns::{MockBlastDNSClient, DnsResolver};\nuse hickory_client::proto::rr::RecordType;\nuse std::collections::HashMap;\n\n// Create a mock client\nlet mut mock_client = MockBlastDNSClient::new();\n\n// Configure mock responses\nlet responses = HashMap::from([\n    (\n        \"example.com\".to_string(),\n        HashMap::from([\n            (\"A\".to_string(), vec![\"93.184.216.34\".to_string()]),\n            (\"AAAA\".to_string(), vec![\"2606:2800:220:1:248:1893:25c8:1946\".to_string()]),\n        ]),\n    ),\n]);\n\n// Hosts that should return NXDOMAIN\nlet nxdomains = vec![\"notfound.example.com\".to_string()];\n\nmock_client.mock_dns(responses, nxdomains);\n\n// Use like any DnsResolver\nlet answers = mock_client.resolve(\"example.com\".to_string(), RecordType::A).await?;\nassert_eq!(answers, vec![\"93.184.216.34\"]);\n\n// NXDOMAIN hosts return empty responses\nlet answers = mock_client.resolve(\"notfound.example.com\".to_string(), RecordType::A).await?;\nassert_eq!(answers.len(), 0);\n```\n\n`MockBlastDNSClient` supports all `DnsResolver` methods including `resolve`, `resolve_full`, `resolve_batch`, `resolve_batch_full`, `resolve_multi`, and `resolve_multi_full`.\n\n### Python API\n\nThe `blastdns` Python package is a thin wrapper around the Rust library.\n\n#### Installation\n\n```bash\n# Using pip\npip install blastdns\n\n# Using uv\nuv add blastdns\n\n# Using poetry\npoetry add blastdns\n```\n\n#### Development Setup\n\n```bash\n# install python dependencies\nuv sync\n# build and install the rust-\u003epython bindings\nuv run maturin develop\n# run tests\nuv run pytest\n```\n\n#### Usage\n\nTo use it in Python, you can use the `Client` class:\n\n```python\nimport asyncio\nfrom blastdns import Client, ClientConfig, DNSResult, DNSError\n\n\nasync def main():\n    resolvers = [\"1.1.1.1:53\"]\n    client = Client(resolvers, ClientConfig(threads_per_resolver=4, request_timeout_ms=1500))\n\n    # resolve: lookup a single host, returns only rdata strings\n    answers = await client.resolve(\"example.com\", \"A\")\n    for answer in answers:\n        print(f\"  {answer}\")  # e.g., \"93.184.216.34\"\n\n    # resolve_full: lookup a single host, returns full DNS response as Pydantic model\n    result = await client.resolve_full(\"example.com\", \"AAAA\")\n    print(f\"Host: {result.host}\")\n    print(f\"Response code: {result.response.header.response_code}\")\n    for answer in result.response.answers:\n        print(f\"  {answer.name_labels}: {answer.rdata}\")\n\n    # resolve_batch: simplified batch resolution with minimal output\n    # returns only (host, record_type, list[rdata]) - no full DNS response structures\n    # automatically filters out errors and empty responses\n    hosts = [\"example.com\", \"google.com\", \"github.com\"]\n    async for host, rdtype, answers in client.resolve_batch(hosts, \"A\"):\n        print(f\"{host} ({rdtype}):\")\n        for answer in answers:\n            print(f\"  {answer}\")  # e.g., \"93.184.216.34\" for A records\n\n    # resolve_batch_full: process many hosts in parallel with full responses\n    # streams results back as they complete\n    hosts = [\"one.example.com\", \"two.example.com\", \"three.example.com\"]\n    async for host, result in client.resolve_batch_full(hosts, \"A\"):\n        if isinstance(result, DNSError):\n            print(f\"{host} failed: {result.error}\")\n        else:\n            print(f\"{host}: {len(result.response.answers)} answers\")\n\n    # resolve_multi: resolve multiple record types for a single host in parallel\n    # returns only successful results with answers\n    record_types = [\"A\", \"AAAA\", \"MX\"]\n    results = await client.resolve_multi(\"example.com\", record_types)\n    for record_type, answers in results.items():\n        print(f\"{record_type}: {answers}\")\n\n    # resolve_multi_full: resolve multiple record types with full response data\n    record_types = [\"A\", \"AAAA\", \"MX\"]\n    results = await client.resolve_multi_full(\"example.com\", record_types)\n    for record_type, result in results.items():\n        if isinstance(result, DNSError):\n            print(f\"{record_type} failed: {result.error}\")\n        else:\n            print(f\"{record_type}: {len(result.response.answers)} answers\")\n\n\nasyncio.run(main())\n```\n\n#### Python API Methods\n\n- **`Client.resolve(host, record_type=None) -\u003e list[str]`**: Lookup a single hostname, returning only rdata strings. Defaults to `A` records. Returns a list of strings (e.g., `[\"93.184.216.34\"]` for A records). Perfect for simple use cases where you just need the record data without the full DNS response structure.\n\n- **`Client.resolve_full(host, record_type=None) -\u003e DNSResult`**: Lookup a single hostname, returning the full DNS response. Defaults to `A` records. Returns a Pydantic `DNSResult` model with typed fields for easy access to headers, queries, answers, etc.\n\n- **`Client.resolve_batch(hosts, record_type=None)`**: Simplified batch resolution that returns only the essential data. Takes an iterable of hostnames and streams back `(host, record_type, answers)` tuples where `answers` is a list of rdata strings (e.g., `[\"93.184.216.34\"]` for A records, `[\"10 aspmx.l.google.com.\"]` for MX records). Automatically filters out errors and empty responses. Perfect for processing large lists of hosts efficiently.\n\n- **`Client.resolve_batch_full(hosts, record_type=None, skip_empty=False, skip_errors=False)`**: Resolve many hosts in parallel with full DNS responses. Takes an iterable of hostnames and streams back `(host, result)` tuples as results complete. Each result is either a `DNSResult` or `DNSError` Pydantic model. Set `skip_empty=True` to filter out successful responses with no answers. Set `skip_errors=True` to filter out error responses.\n\n- **`Client.resolve_multi(host, record_types) -\u003e dict[str, list[str]]`**: Resolve multiple record types for a single hostname in parallel, returning only successful results with answers. Takes a list of record type strings (e.g., `[\"A\", \"AAAA\", \"MX\"]`) and returns a dictionary mapping record types to lists of rdata strings. Only includes record types that resolved successfully and have answers.\n\n- **`Client.resolve_multi_full(host, record_types) -\u003e dict[str, DNSResultOrError]`**: Resolve multiple record types for a single hostname in parallel, returning full DNS responses. Takes a list of record type strings and returns a dictionary keyed by record type. Each value is either a `DNSResult` (success) or `DNSError` (failure) Pydantic model. Includes all record types, even those that failed or had no answers.\n\n#### MockClient for Testing\n\n`MockClient` provides a drop-in replacement for `Client` that returns fabricated DNS responses without making real network requests. It implements the same interface as `Client` and is useful for testing code that depends on DNS lookups.\n\n```python\nimport pytest\nfrom blastdns import MockClient, DNSResult\n\n\n@pytest.fixture\ndef mock_client():\n    \"\"\"Create a mock client with pre-configured test data.\"\"\"\n    client = MockClient()\n    client.mock_dns({\n        \"example.com\": {\n            \"A\": [\"93.184.216.34\"],\n            \"AAAA\": [\"2606:2800:220:1:248:1893:25c8:1946\"],\n            \"MX\": [\"10 aspmx.l.google.com.\", \"20 alt1.aspmx.l.google.com.\"],\n        },\n        \"cname.example.com\": {\n            \"CNAME\": [\"example.com.\"]\n        },\n        \"_NXDOMAIN\": [\"notfound.example.com\"],  # hosts that return NXDOMAIN\n    })\n    return client\n\n\n@pytest.mark.asyncio\nasync def test_my_function(mock_client):\n    # resolve() returns simple rdata strings\n    answers = await mock_client.resolve(\"example.com\", \"A\")\n    assert answers == [\"93.184.216.34\"]\n\n    # resolve_full() returns full DNS response structure\n    result = await mock_client.resolve_full(\"example.com\", \"A\")\n    assert isinstance(result, DNSResult)\n    assert len(result.response.answers) == 1\n\n    # NXDOMAIN hosts return empty responses (not errors)\n    answers = await mock_client.resolve(\"notfound.example.com\", \"A\")\n    assert len(answers) == 0\n\n    # resolve_batch() works with all mocked hosts\n    async for host, rdtype, answers in mock_client.resolve_batch([\"example.com\"], \"A\"):\n        print(f\"{host}: {answers}\")  # [\"93.184.216.34\"]\n\n    # resolve_multi() resolves multiple record types in parallel\n    results = await mock_client.resolve_multi(\"example.com\", [\"A\", \"AAAA\", \"MX\"])\n    assert len(results) == 3\n    assert results[\"MX\"] == [\"10 aspmx.l.google.com.\", \"20 alt1.aspmx.l.google.com.\"]\n```\n\n**Key Features:**\n- Supports all `Client` methods: `resolve`, `resolve_full`, `resolve_batch`, `resolve_batch_full`, `resolve_multi`, `resolve_multi_full`\n- Returns the same data structures as `Client` for drop-in compatibility\n- NXDOMAIN hosts (specified in `_NXDOMAIN` list) return empty responses, not errors\n- Unmocked hosts also return empty responses\n- Auto-formats PTR queries (IP addresses → reverse DNS format) just like the real client\n\n#### Response Models\n\nThe `*_full()` methods return Pydantic V2 models for type safety and IDE autocomplete:\n\n- **`DNSResult`**: Successful DNS response with `host` and `response` fields\n- **`DNSError`**: Failed DNS lookup with an `error` field\n- **`Response`**: DNS message with `header`, `queries`, `answers`, `name_servers`, etc.\n\nThe base methods (`resolve`, `resolve_batch`, `resolve_multi`) return simple Python types (lists, dicts, strings) for convenience when you don't need the full response structure.\n\n`ClientConfig` exposes the knobs shown above (`threads_per_resolver`, `request_timeout_ms`, `max_retries`, `purgatory_threshold`, `purgatory_sentence_ms`) and validates them before handing them to the Rust core.\n\n## Architecture\n\nBlastDNS is built on top of [`hickory-dns`](https://github.com/hickory-dns/hickory-dns), but only makes use of the low-level Client API, not the Resolver API.\n\nBeneath the hood of the `BlastDNSClient`, each resolver gets its own `ResolverWorker` tasks, with a configurable number of workers per resolver (default: 2, configurable via `BlastDNSConfig.threads_per_resolver`).\n\nWhen a user calls `BlastDNSClient::resolve`, a new `WorkItem` is created which contains the request (host + rdtype) and a oneshot channel to hold the result. This `WorkItem` is put into a [crossfire](https://github.com/frostyplanet/crossfire-rs) MPMC queue, to be picked up by the first available `ResolverWorker`. Workers are spawned lazily when the first request is made.\n\n### Caching\n\nBlastDNS includes an optional TTL-aware cache using an LRU eviction policy. The cache is enabled by default with a capacity of 10,000 entries and can be configured or disabled entirely:\n\n- Only **positive responses with answers** are cached (no errors, NXDOMAIN, or empty responses)\n- Cache entries automatically expire based on DNS record TTLs (clamped to configurable min/max bounds)\n- Expired entries are removed on access\n- Thread-safe with minimal lock contention\n\nConfigure via `BlastDNSConfig`:\n- `cache_capacity`: Number of entries (default: 10000, set to 0 to disable)\n- `cache_min_ttl`: Minimum TTL (default: 10 seconds)\n- `cache_max_ttl`: Maximum TTL (default: 1 day)\n\n### Retry Logic and Fault Tolerance\n\nBlastDNS handles unreliable resolvers through a multi-layered retry system:\n\n**Client-Level Retries**: When a query fails with a retryable error (network timeouts, connection failures), the client automatically retries up to `max_retries` times (default: 10). Each retry creates a fresh `WorkItem` and sends it back to the shared queue, where it can be picked up by **any available worker**—not necessarily the same resolver. This means retries naturally route around problematic resolvers.\n\n**Purgatory System**: Each worker tracks consecutive errors. After hitting `purgatory_threshold` failures (default: 10), the worker enters \"purgatory\"—it sleeps for `purgatory_sentence` milliseconds (default: 1000ms) before resuming work. This temporarily sidelines struggling resolvers without removing them entirely, allowing the system to self-heal if resolver issues are transient.\n\n**Non-Retryable Errors**: Configuration errors (invalid hostnames) and system errors (queue closed) fail immediately without retry, preventing wasted work on queries that can't succeed.\n\nThis architecture ensures maximum accuracy even with a mixed pool of reliable and unreliable DNS servers, as queries naturally migrate toward responsive resolvers while problematic ones throttle themselves.\n\n## Testing\n\nBlastDNS has two types of tests:\n\n### Unit Tests (No DNS Server Required)\n\nUnit tests use `MockBlastDNSClient` (Rust) or `MockClient` (Python) and run without any external dependencies:\n\n```bash\n# Rust unit tests\ncargo test\n\n# Python unit tests\nuv run pytest\n```\n\n### Integration Tests (Require DNS Server)\n\nIntegration tests verify real DNS resolution against a local `dnsmasq` server running on `127.0.0.1:5353` and `[::1]:5353`.\n\nInstall `dnsmasq`:\n\n```bash\nsudo apt install dnsmasq\n```\n\nStart the test DNS server:\n\n```bash\nsudo ./scripts/start-test-dns.sh\n```\n\nRun integration tests:\n\n```bash\n# Rust integration tests (marked with #[ignore])\ncargo test -- --ignored\n\n# Python integration tests with real DNS\nuv run pytest -k \"not mock\"\n```\n\nWhen done, stop the test DNS server:\n\n```bash\n./scripts/stop-test-dns.sh\n```\n\n## Linting\n\n### Rust\n\n```bash\n# Run clippy for lints\ncargo clippy --all-targets --all-features\n\n# Run rustfmt for formatting\ncargo fmt --all\n```\n\n### Python\n\n```bash\n# Run ruff for lints\nuv run ruff check --fix\n\n# Run ruff for formatting\nuv run ruff format\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fblacklanternsecurity%2Fblastdns","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fblacklanternsecurity%2Fblastdns","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fblacklanternsecurity%2Fblastdns/lists"}