Every DNS transport. One .NET client. DnsClientX is an async DNS library and PowerShell module for UDP, TCP, DNS over TLS, HTTPS, HTTP/3 and QUIC, with typed records, local DNSSEC validation, multi-resolver strategies, zone transfers, dynamic updates and service discovery. The core has no dependencies on .NET 8 and .NET 10, and it also runs on .NET Standard 2.0 and .NET Framework 4.7.2.
Website · Guides · DNS playground · .NET API · PowerShell cmdlets · Roadmap · Discord
dotnet add package DnsClientXInstall-Module DnsClientX -Scope CurrentUserThe 2.2.0 release includes the core NuGet package, PowerShell module and CLI archives. DnsClientX.Odoh, DnsClientX.DnsCrypt and DnsClientX.DnsSec.EdDsa are not published on NuGet and are not included in these artifacts. Their examples require a provider built from the same source revision, with its dependencies deployed beside the provider DLL. Core DNS queries, DNSSEC algorithms supported by the running target, and the module work without these providers.
using DnsClientX;
var response = await ClientX.QueryDns("evotec.pl", DnsRecordType.MX, DnsEndpoint.Cloudflare);
foreach (var answer in response.Answers) {
Console.WriteLine($"{answer.Name} {answer.TTL} {answer.Data}");
}Resolve-Dns -Name evotec.pl -Type MX -DnsProvider Cloudflare
Resolve-Dns -Name evotec.pl -Type A -DnsProvider Cloudflare,Quad9 -ResolverStrategy FirstSuccess| Area | What you get | Guide |
|---|---|---|
| Transports | UDP with TCP fallback, TCP, DoT, DoH wire and JSON over GET or POST, HTTP/2, DoH3, DoQ, gRPC and multicast DNS behind one endpoint syntax | Transports and endpoints |
| Records | Raw strings or typed objects with owner, class, remaining TTL and the original wire evidence | .NET guide |
| DNSSEC | Local chain validation to the IANA root (Secure, Insecure, Bogus, Indeterminate), authenticated denial, iterative root resolution, QNAME minimisation and RFC 5011 anchors |
DNSSEC and resolution |
| Many resolvers | FirstSuccess, FastestWins, SequentialFallback and RoundRobin with concurrency caps, single-flight caching, probes, benchmarks and saved scores | Multi-resolver and benchmarking |
| Zones | AXFR, IXFR, XFR-over-TLS, ZONEMD, TSIG, a BIND master-file parser and atomic RFC 2136 UPDATE batches | Zones and updates |
| Discovery | DNS-SD over unicast and multicast, continuous mDNS browsing and DDR discovery of verified encrypted resolvers | Service discovery |
| Your network | System resolver discovery with Windows NRPT, resolv.conf search lists, EDNS client subnet, DNS stamps and source binding | Transports and endpoints |
| Browser | A caller-owned HttpClient DNS JSON client for Blazor WebAssembly and dependency-injected hosts |
Try it live |
| Surfaces | The .NET library, a PowerShell module with 20 cmdlets for Windows PowerShell 5.1 and PowerShell 7.4+, and a CLI | PowerShell guide |
Note
Version 2.2.0 includes the CLI, designated-resolver discovery and the newer cmdlets (Test-DnsHealth, Get-DnsDesignatedResolver, Receive-DnsService, Get-DnsQueryExplanation, Compare-DnsResolverSnapshot, Get-DnsCache and Clear-DnsCache). The optional DnsClientX.Odoh, DnsClientX.DnsCrypt and DnsClientX.DnsSec.EdDsa projects remain source-only: they are not published on NuGet and are excluded from the 2.2.0 artifacts. See the changelog for released versions.
- DomainDetective resolves every check through DnsClientX, including its browser tools, where DnsClientX runs as WebAssembly.
DnsClientX is part of the Evotec open-source toolkit, alongside DomainDetective, OfficeIMO, ChartForgeX, CodeGlyphX, HtmlForgeX and TestimoX.
DnsClientX gives you two ways to work with DNS query results, depending on your needs:
Returns DNS data as normalized text. Use DataRaw when you need the provider's original presentation:
var response = await ClientX.QueryDns("google.com", DnsRecordType.MX, DnsEndpoint.Cloudflare);
foreach (var answer in response.Answers) {
Console.WriteLine($"Normalized data: {answer.Data}"); // "10 smtp.google.com"
}Automatically parses DNS data into strongly-typed .NET objects with properties:
var response = await client.Resolve("google.com", DnsRecordType.MX, typedRecords: true);
foreach (var answer in response.TypedAnswers!) {
if (answer is MxRecord mx) {
Console.WriteLine($"Mail server: {mx.Exchange}, Priority: {mx.Preference}");
}
}When to use each approach:
- Non-typed: Quick queries, simple data extraction, minimal processing
- Typed: Complex applications, structured data processing, type safety, IntelliSense support
The main Resolve APIs accept typedRecords: true. Supported records are returned as dedicated types; other records remain available as UnknownRecord, so opting into typed parsing never requires pretending an unsupported parser is complete.
Select a built-in provider profile and optionally choose a published transport. Existing preset values and default transports are preserved. Configuration(provider, requestFormat) and DnsResolverEndpointFactory.From(provider, requestFormat) select alternatives while retaining the provider's filtering profile and TLS identity.
| Transport | Implementation | Runtime |
|---|---|---|
| UDP, TCP, DoT, DoH wire/JSON | Core library | All supported targets |
| DoH3, DoQ | Core library | .NET 8/10 with runtime QUIC support |
| DNSCrypt v2, Anonymized DNSCrypt | Optional DNSCrypt provider | Provider dependencies deployed by the caller |
| ODoH | Optional ODoH provider | Explicit HTTPS target and relay |
The generated table lists published transport mappings available through the built-in profiles. A dash means the profile has no mapping for that transport. These are service definitions, not a live connectivity or filtering-policy test. DoH wire includes RFC 8484 GET and POST. Runtime capability is checked separately with DnsTransportCapabilities.
Resolve-Dns example.com -DnsProvider AdGuardFamily -ProviderTransport DnsOverQuic
Resolve-Dns example.com -DnsProvider Cloudflare -ProviderTransport DnsOverTLSDnsClientX --endpoint ControlDAds --provider-transport DnsOverQuic example.com
Regenerate the table and the machine-readable provider catalog after a library build with pwsh -File Build/Update-ProviderDocumentation.ps1. The JSON uses schema 1, enum-ordered profiles and the same seven transport columns in the same order. Its default values are request-format enum names. Use -Check to verify both committed outputs against the library without writing files; packaged-module validation runs this check too.
Selecting UDP or TCP for System preserves the operating system's resolver policy, including in probes and benchmarks. RootServer remains an iterative UDP profile and rejects a TCP override. Use an explicit TCP endpoint to query an individual root server directly.
For Resolve-Dns, transport overrides involving System, SystemTcp, or RootServer require a single-provider query. Multi-resolver comparisons use explicit endpoints and cannot retain those profiles' system-policy or iterative behavior.
| Profile | Default | UDP | TCP | DoT | DoH wire | DoH JSON | DoH3 | DoQ |
|---|---|---|---|---|---|---|---|---|
| System | UDP | yes | yes | - | - | - | - | - |
| SystemTcp | TCP | yes | yes | - | - | - | - | - |
| Cloudflare | DoH JSON | yes | yes | yes | yes | yes | - | - |
| CloudflareSecurity | DoH JSON | yes | yes | yes | yes | yes | - | - |
| CloudflareFamily | DoH JSON | yes | yes | yes | yes | yes | - | - |
| CloudflareWireFormat | DoH wire | yes | yes | yes | yes | yes | - | - |
| CloudflareWireFormatPost | DoH POST | yes | yes | yes | yes | yes | - | - |
| DoH wire | yes | yes | yes | yes | yes | - | - | |
| GoogleWireFormat | DoH wire | yes | yes | yes | yes | yes | - | - |
| GoogleWireFormatPost | DoH POST | yes | yes | yes | yes | yes | - | - |
| Quad9 | DoH wire | yes | yes | yes | yes | - | yes | yes |
| Quad9ECS | DoH wire | yes | yes | yes | yes | - | yes | yes |
| Quad9Unsecure | DoH wire | yes | yes | yes | yes | - | yes | yes |
| OpenDNS | DoH wire | yes | yes | - | yes | - | - | - |
| OpenDNSFamily | DoH wire | yes | yes | - | yes | - | - | - |
| Quad9Http3 | DoH3 | yes | yes | yes | yes | - | yes | yes |
| Quad9Quic | DoQ | yes | yes | yes | yes | - | yes | yes |
| AdGuard | DoH wire | yes | yes | yes | yes | - | - | yes |
| AdGuardFamily | DoH wire | yes | yes | yes | yes | - | - | yes |
| AdGuardNonFiltering | DoH wire | yes | yes | yes | yes | - | - | yes |
| NextDNS | DoH JSON | yes | yes | - | yes | yes | - | - |
| RootServer | UDP | yes | - | - | - | - | - | - |
| CleanBrowsingSecurity | DoH wire | yes | yes | yes | yes | - | - | - |
| CleanBrowsingAdult | DoH wire | yes | yes | yes | yes | - | - | - |
| CleanBrowsingFamily | DoH wire | yes | yes | yes | yes | - | - | - |
| ControlD | DoH wire | yes | yes | yes | yes | - | - | yes |
| ControlDMalware | DoH wire | yes | yes | yes | yes | - | - | yes |
| ControlDAds | DoH wire | yes | yes | yes | yes | - | - | yes |
| ControlDFamily | DoH wire | yes | yes | yes | yes | - | - | yes |
| ComodoSecureDns | UDP | yes | yes | - | - | - | - | - |
| DnsWatch | UDP | yes | yes | - | - | - | - | - |
CloudflareQuic, GoogleQuic, CloudflareJsonPost, GoogleJsonPost, CloudflareOdoh, and the DNSCrypt preset names are retained for source compatibility and always throw when passed to the built-in endpoint constructor. The core implements DoQ for published endpoints such as Quad9Quic without extra transport packages on modern .NET. Create explicit caller-owned configurations through DnsClientX.Odoh or DnsClientX.DnsCrypt to use privacy transports.
Choose the profile whose policy you want applied to your queries. A provider's unfiltered service and family service can return different answers by design.
| Preset | Policy and endpoint |
|---|---|
CleanBrowsingSecurity |
Malicious-domain filtering, without adult-content filtering or SafeSearch; https://doh.cleanbrowsing.org/doh/security-filter/ |
CleanBrowsingAdult |
Adult-content and security filtering with SafeSearch; https://doh.cleanbrowsing.org/doh/adult-filter/ |
CleanBrowsingFamily |
Also blocks mixed-content sites and proxy/VPN domains, and enforces SafeSearch; https://doh.cleanbrowsing.org/doh/family-filter/ |
ControlD |
Unfiltered; https://freedns.controld.com/p0 |
ControlDMalware |
Malware filtering; https://freedns.controld.com/p1 |
ControlDAds |
Ads and tracking filtering; https://freedns.controld.com/p2 |
ControlDFamily |
Family-friendly filtering; https://freedns.controld.com/family |
ComodoSecureDns |
Plain DNS to 8.26.56.26 and 8.20.247.20 |
DnsWatch |
Plain DNS to 84.200.69.80, 84.200.70.40, 2001:1608:10:25::1c04:b12f, and 2001:1608:10:25::9249:d69b |
ComodoSecureDns and DnsWatch use UDP port 53 with the usual TCP fallback; queries are not encrypted. DnsWatch selects its first IPv4 address by default; the other IPv4 and IPv6 addresses are available through DnsSelectionStrategy and multi-resolver expansion. Use explicit endpoints when you need to restrict queries to IPv4 or IPv6.
CleanBrowsing's 185.228.168.9 / 185.228.169.9 pair selects its Security filter. Control D's 76.76.2.0 / 76.76.10.0 pair is unfiltered. OpenDNS FamilyShield uses 208.67.222.123 / 208.67.220.123, represented by OpenDNSFamily.
Provider policies and addresses are documented by CleanBrowsing, Control D, Comodo, DNS.WATCH, and OpenDNS.
var response = await ClientX.QueryDns("example.com", DnsRecordType.A, DnsEndpoint.CleanBrowsingFamily);Resolve-Dns -Name example.com -Type A -DnsProvider CleanBrowsingFamily
Resolve-Dns -Name example.com -Type A -DnsProvider ControlDAds
DnsClientX.Cli --endpoint ControlDMalware --type A example.comThe NextDNS preset is a generic resolver and does not select your personal filtering configuration. For example, switching from -DnsProvider NextDNS to -ResolverEndpoint 'doh@https://dns.nextdns.io/abc123' selects configuration abc123. Replace that example ID with the one from your NextDNS setup page. The complete URL path is preserved, including an optional device name.
Resolve-Dns -Name example.com -Type A -ResolverEndpoint 'doh@https://dns.nextdns.io/abc123'
DnsClientX.Cli --resolver 'doh@https://dns.nextdns.io/abc123' --type A example.comvar endpoints = EndpointParser.TryParseMany(
new[] { "doh@https://dns.nextdns.io/abc123" }, out var errors);
if (errors.Count != 0) throw new ArgumentException(string.Join("; ", errors));
using var client = ResolverEndpointClientFactory.CreateClient(endpoints[0]);
var response = await client.Resolve("example.com", DnsRecordType.A);For DoT, use the profile hostname supplied by NextDNS, such as dot@abc123.dns.nextdns.io:853. The shared explicit-endpoint syntax also accepts personal Control D or other provider URLs; no provider-specific parameter is required. NextDNS's own client uses the same profile-specific DoH URL shape.
If you want to learn about DNS:
Note
DnsClientX normalizes presentation details such as trailing dots and TXT character-string concatenation, but preserves DNS resource-record boundaries. Resolver answers can legitimately differ because of cache state, anycast location, ECS, filtering policy, or propagation; comparison code should not assume every resolver returns an identical RRset at the same instant.
For TXT and SPF records, DataRaw retains the server's presentation, DataStrings retains its quoted character-strings, and DataStringsEscaped exposes the decoded strings. Typed TxtRecord objects provide the same views through RawText, RawStrings, and Strings; their fragment collections are read-only. Data and TxtRecord.Text concatenate the decoded strings within the same resource record, without adding separators. Spaces, empty chunks, escaped quotes, and payload line breaks are preserved. Use TxtConcatenatedData when you deliberately want TXT display output with line breaks removed. Opaque and application record payloads retain their case.
Supported wire record types also accept generic hexadecimal RDATA (\# <length> <hex>) through the same decoder as binary DNS responses. The declared octet length must match the payload. Unknown types and malformed generic encodings retain their raw presentation and project as UnknownRecord.
For name, mail, routing, and authority records such as NS, CNAME, MX, SOA, and SRV, Data normalizes DNS name fields to lowercase without a trailing root dot, retaining . for the root itself. Normalized records use one space between parsed fields and preserve spaces inside quoted payloads. DNSKEY public keys retain their case and all encoded fragments; DS, TLSA, SMIMEA, and SSHFP hexadecimal payloads use uppercase without presentation whitespace. LOC text accepts omitted minutes, seconds, and precision fields with their standard defaults. DataRaw always retains the provider's original representation, including NSEC type spellings. Record types without a semantic normalizer retain their presentation text; callers can use DataRaw for the original evidence on every record type.
Every supported transport returns the same DnsResponse and record model. DnsAnswer.Data contains normalized record data, while DataRaw retains the provider's original presentation. CLI JSON includes both: the existing data field stays raw, and data_normalized contains the normalized value. TXT character-string boundaries and original spelling remain available in the raw field.
{
"name": "www.example.com",
"type": "CNAME",
"data": "Target.Example.",
"data_normalized": "target.example"
}ExtendedDnsErrors provides structured EDE codes and text from wire responses and JSON providers. When JSON supplies only recognized EDE(code): text comments, those comments also populate ExtendedDnsErrors. Explicit structured errors take precedence, and Comments retains its original text. Plain prose and malformed codes are not interpreted as error evidence.
Normal CLI text output includes available comments and extended DNS errors, showing exact repetitions once. --short remains answer-only output. Use --explain for resolver and execution details, or PowerShell's -FullResponse to inspect the complete response:
$response = Resolve-Dns -Name example.com -Type A -DnsProvider Cloudflare -FullResponse
$response | Select-Object Status, Error, Comments, ExtendedDnsErrorInfo
$response.Answers | Select-Object Name, Type, Data, DataRawPowerShell warnings include the DNS status and available diagnostics for non-success replies. Provider policy remains visible: NoError with a zero address, NXDOMAIN, and REFUSED can represent different filtering choices. DnsClientX preserves those response codes and records; it does not infer that every NXDOMAIN is a block or that every NoError answer is allowed.
- .NET 8.0 / .NET 10.0 (Windows, Linux, macOS)
- No external dependencies
- DoH3 and DoQ stay in the core package with no added NuGet transport dependencies
- .NET Standard 2.0 (Cross-platform compatibility)
- System.Text.Json (10.0.12)
- Microsoft.Bcl.AsyncInterfaces (10.0.12)
- .NET Framework 4.7.2 (Windows only)
- System.Net.Http (built-in)
- System.Text.Json (10.0.12)
- Microsoft.Bcl.AsyncInterfaces (10.0.12)
- Modern transports such as DoH3 and DoQ are exposed by the shared API surface but return unsupported at runtime
- .NET 8.0 / .NET 10.0 (Windows, Linux, macOS)
- .NET Framework 4.7.2 (Windows only)
- Framework-dependent single-file deployment for x64 and ARM64 on Windows, Linux, and macOS; install the matching .NET 8 runtime
- Build distributable archives through
Invoke-DotNetPublish -ConfigPath ./Build/cli.publish.jsonin PSPublishModule
- PowerShell 7.4 or later on Windows, Linux, and macOS uses the .NET 8 build
- Windows PowerShell 5.1 uses the .NET Framework 4.7.2 build
- PowerShellStandard.Library (5.1.1)
- .NET 8.0 only
- Spectre.Console (0.57.2) for enhanced console output
Cross-Platform Testing: All tests run simultaneously across Windows, Linux, and macOS to ensure compatibility.
- Supports multiple built-in DNS Providers (System, Cloudflare, Google, Quad9, OpenDNS, etc.)
- Supports both JSON and Wireformat
- Supports DNS over HTTPS (DoH) using GET and POST methods
- Supports DNS over HTTP/3 (DoH3) on .NET 8 and .NET 10
- Supports DNS over TLS (DoT)
- Supports DNS over QUIC (DoQ) on .NET 8 and .NET 10
- Supports DNS over UDP, and switches to TCP if needed
- Supports DNS over TCP
- Supports DNSSEC
- Validates DNSSEC locally through root trust anchors, signed RRsets, and authenticated NSEC/NSEC3 denial proofs
- Authenticates RFC 2136 DNS UPDATE requests and responses with TSIG (HMAC-SHA-256/384/512 and SHA-1 compatibility)
- Supports multiple DNS record types
- Supports parallel queries
- No external dependencies on .NET 8 and .NET 10
- Minimal dependencies on .NET Standard 2.0 and .NET 4.7.2
- Implements IDisposable/IAsyncDisposable to release cached HTTP, UDP, and QUIC transport resources
- Multi-line record data normalized to use
\nline endings - Supports DNS Service Discovery (DNS-SD)
- Streams continuous mDNS service additions, changes, goodbyes, and expirations
- Discovers designated encrypted resolvers with SVCB and verifies their original resolver IP identity
- Sends atomic RFC 2136 UPDATE batches with name and RRset prerequisites
- Authenticates multi-message AXFR and IXFR transfers with TSIG
- Accepts explicit DNSSEC trust anchors for private zones
DnsClientX keeps modern transports in the core package instead of moving them behind extra transport packages:
- DoH3 (
DnsOverHttp3) is available in the core package on .NET 8 and .NET 10 with no extra NuGet dependencies. - DoQ (
DnsOverQuic) is available in the core package on .NET 8 and .NET 10 when the runtime exposes QUIC support, also with no extra NuGet dependencies. - .NET Standard 2.0 / .NET Framework 4.7.2 keep the same API surface, but modern transports are reported as unsupported at runtime instead of pulling in separate QUIC stacks.
Use DnsTransportCapabilities when you want to check support before executing a query.
PowerShell:
Get-DnsTransportCapability
Get-DnsTransportCapability -ModernOnlyCLI:
DnsClientX.Cli --capabilities
DnsClientX.Cli --capabilities --format jsonSaved probe and benchmark snapshots also persist runtime capability hints, so recommendation files can distinguish unsupported modern transports from ordinary resolver failures. Snapshots include a schema version, and resolver selection rejects snapshots from newer unsupported schemas before attempting reuse. Saved recommendations preserve explicit endpoint settings, including TLS name, address family, timeout, EDNS and request format. When a display label represents several different configurations, the snapshot retains the observations and reports that automatic reuse and scope comparison are unavailable.
ODoH encrypts application DNS queries for a target and sends them through a separate HTTPS relay. Install DnsClientX.Odoh and configure both endpoints:
using DnsClientX;
using DnsClientX.Odoh;
using var provider = new OdohClient(new OdohClientOptions(
new Uri("https://relay.example/dns-query"),
new Uri("https://target.example/dns-query")));
using var client = new ClientX(provider.CreateConfiguration());
var response = await client.Resolve("example.com", DnsRecordType.A);The provider supports the RFC 9230 X25519/HKDF-SHA256/AES-128-GCM suite, refreshes target configurations obtained through HTTPS, and authenticates each response against its own query. Its default HTTP client disables cookies and redirects. Failed exchanges never fall back to direct DoH or plaintext DNS. Relay/target collusion and traffic analysis remain outside this protection. See the ODoH package guide for supplied HTTP-client requirements, padding, and configuration lifetimes.
DNSCrypt v2 authenticates resolver certificates with a trusted provider public key and encrypts application queries using the NaCl XSalsa20 or XChaCha20 construction. Install DnsClientX.DnsCrypt, obtain the resolver metadata through a trusted source, and keep the provider alive while its clients run:
using DnsClientX;
using DnsClientX.DnsCrypt;
var options = new DnsCryptOptions(
resolverAddress, 443, providerName, providerPublicKeyHex);
using var provider = new DnsCryptClient(options);
using var client = provider.CreateClient();
var response = await client.Resolve("example.com", DnsRecordType.A);The optional DNSCrypt package includes libsodium native assets for supported desktop platforms. NetworkTransport chooses encrypted UDP with TCP recovery or TCP directly; RelayEndpoint enables Anonymized DNSCrypt. Certificate discovery sends the provider TXT question, while application queries remain encrypted. See the DNSCrypt package guide for cipher requirements, relay behavior, native runtime support, and refresh policy. Both optional providers implement IDnsWireTransport; the core retains response correlation, record parsing, and DNSSEC validation without taking ownership of the provider lifetime.
Custom resolver inputs share one endpoint parser across the library, CLI, and PowerShell.
- UDP:
udp@1.1.1.1:53 - TCP:
tcp@9.9.9.9:53 - DoT:
dot@dns.quad9.net:853 - DoH:
doh@https://dns.google/dns-query - DoH3:
doh3@https://dns.quad9.net/dns-query - DoQ:
doq@dns.quad9.net:853 - DNS stamp:
sdns://AgUAAAAAAAAABzEuMS4xLjEAGm1vemlsbGEuY2xvdWRmbGFyZS1kbnMuY29tCi9kbnMtcXVlcnk
The same syntax works in:
Resolve-Dns -ResolverEndpoint ...Test-DnsProbe -ResolverEndpoint ...Test-DnsBenchmark -ResolverEndpoint ...- CLI probe and benchmark endpoint inputs
DNS stamps currently map into the core endpoint model for plain DNS, DoH, DoT, and DoQ. Privacy stamps are inspected with DnsStamp.ParsePrivacy, ConvertFrom-DnsStamp, or CLI --stamp-info. The optional provider factories consume DNSCrypt resolver/relay and ODoH target/relay identities; the core endpoint parser continues to reject privacy stamps where an explicit provider is required.
The CLI is intended for quick diagnostics and automation-friendly resolver checks.
Standard query output:
DnsClientX.Cli --format json example.com
DnsClientX.Cli --format raw --question --answer --authority --additional example.com
DnsClientX.Cli --short example.com
DnsClientX.Cli --txt-concat --type TXT example.com
DnsClientX.Cli --reverse 1.1.1.1Human-readable output, including --short and --format raw, displays control and Unicode formatting characters as \uXXXX escapes. Use --format json when automation needs the original DNS data; parsed JSON values retain those characters.
Zone transfer convenience:
DnsClientX.Cli --axfr --transfer-summary example.com
DnsClientX.Cli --axfr --format json example.comResolver import, probe, benchmark, and score reuse:
DnsClientX.Cli --resolver-validate --resolver-file .\resolvers.txt
DnsClientX.Cli --resolver-validate --resolver-file .\resolvers.txt --format json
Test-DnsResolverCatalog -ResolverEndpointFile .\resolvers.txt
DnsClientX.Cli --probe --resolver-file .\resolvers.txt --probe-save .\probe.json
DnsClientX.Cli --benchmark --resolver-url https://example.com/resolvers.txt --benchmark-save .\benchmark.json
DnsClientX.Cli --resolver-select .\probe.json
DnsClientX.Cli --resolver-use .\probe.json --short example.comDirect resolver queries and structured JSON diagnostics:
DnsClientX.Cli --resolver tcp@1.1.1.1:53 --format json --explain example.com
DnsClientX.Cli --probe --probe-endpoint udp@1.1.1.1:53 --format json example.com
DnsClientX.Cli --benchmark --probe-endpoint tcp@1.1.1.1:53 --format json example.comJSON stdout contains one complete document, including structured explanation or trace when requested. Endpoint inputs require an explicit --probe or --benchmark for those modes; ordinary queries can use --resolver. Explanations show observed resolver, cache, bootstrap, aliases, attempts and local DNSSEC dependencies. They identify unavailable evidence and do not infer a complete trust chain or delegation traversal.
Repeatable health profiles define local, versioned query catalogs and policies:
{
"SchemaVersion": 1,
"Name": "public-dns-health",
"ResolverEndpoints": ["udp@1.1.1.1:53", "tcp@8.8.8.8:53"],
"Names": ["example.com", "example.net"],
"RecordTypes": ["A", "AAAA"],
"TimeoutMs": 2000,
"MaxConcurrency": 4,
"ProbePolicy": { "MinSuccessCount": 1, "MinSuccessPercent": 50 }
}DnsClientX.Cli --profile ./dns-health.json --benchmark --profile-save ./health.json --format json
Test-DnsHealth ./dns-health.json -IncludeBenchmark -FailOnPolicyViolation -ErrorAction Stop
DnsClientX.Cli --resolver-compare ./before.json ./after.json --format json
Compare-DnsResolverSnapshot ./before.json ./after.jsonProfiles are bounded local JSON documents. Settings come from the profile; C# and PowerShell can modify the typed profile before running it. Comparisons use probe/benchmark score snapshots, report candidate and answer changes, and require matching host, runtime, query catalog and policy scope for health or latency conclusions. Older snapshots remain readable but may lack the comparison context. Resolver agreement is evidence of consistency. For authoritative comparison, supply explicit caller-owned authoritative responses; a majority answer does not establish correctness.
Profiles reject endpoints that identify the same execution. Probe-only profiles can compare different settings on the same server. If those endpoints share a display label, run them in separate benchmark profiles; combined benchmark mode rejects the ambiguous inputs before sending queries.
DNS stamp inspection without querying DNS:
DnsClientX.Cli --stamp-info 'sdns://AgUAAAAAAAAABzEuMS4xLjEAGm1vemlsbGEuY2xvdWRmbGFyZS1kbnMuY29tCi9kbnMtcXVlcnk'
DnsClientX.Cli --stamp-info 'sdns://AgUAAAAAAAAABzEuMS4xLjEAGm1vemlsbGEuY2xvdWRmbGFyZS1kbnMuY29tCi9kbnMtcXVlcnk' --format json
ConvertFrom-DnsStamp -Stamp 'sdns://AgUAAAAAAAAABzEuMS4xLjEAGm1vemlsbGEuY2xvdWRmbGFyZS1kbnMuY29tCi9kbnMtcXVlcnk'The test suite includes limited live checks for DoH3 and DoQ. They are opt-in so default CI stays stable.
Set both environment variables before running tests:
$env:DNSCLIENTX_RUN_REAL_DNS_TESTS = '1'
$env:DNSCLIENTX_RUN_MODERN_TRANSPORT_TESTS = '1'
dotnet test .\DnsClientX.Tests\DnsClientX.Tests.csproj --filter FullyQualifiedName~ModernTransportRealTestsThese live tests only run on .NET 8 and later and stay focused on a small set of real endpoints.
When querying DNS records using DnsClientX, you may notice that different DNS providers can return different results for the same domain. This is normal behavior and occurs for several legitimate reasons:
Many popular websites use CDNs (like Cloudflare, Akamai, AWS CloudFront) to serve content from servers geographically closer to users. CDN-backed domains will return different IP addresses based on:
- Geographic location: Providers route queries to the nearest edge server
- Provider routing policies: Different DNS providers may have different relationships with CDNs
- Load balancing: CDNs dynamically distribute traffic across multiple servers
Example: A domain like www.example.com might return:
- From Cloudflare:
23.47.124.71,23.47.124.85 - From OpenDNS:
185.225.251.105,185.225.251.40
Both responses are correct - they're just optimized for different network paths.
Different DNS providers have distinct characteristics:
- Cloudflare (1.1.1.1): Privacy-focused, fast, global Anycast network
- Google (8.8.8.8): Extensive caching, Google's global infrastructure
- Quad9 (9.9.9.9): Security-focused, blocks malicious domains
- OpenDNS: Content filtering options, enterprise features
| Provider | Hostname | Request Format |
|---|---|---|
| Cloudflare | 1.1.1.1 / 1.0.0.1 |
JSON |
dns.google |
RFC 8484 wire | |
| Quad9 | dns.quad9.net |
Wire |
| OpenDNS | 208.67.222.222 / 208.67.220.220 |
Wire |
| AdGuard | dns.adguard-dns.com |
Wire |
| AdGuardFamily | family.adguard-dns.com |
Wire |
| AdGuardNonFiltering | unfiltered.adguard-dns.com |
Wire |
These differences can result in:
- Varying response times (typically 10-500ms)
- Different cached TTL values
- Different IP addresses for CDN domains
- Slightly different DNSSEC validation results
You should expect consistent results for:
- Non-CDN domains: Simple domains with static IP assignments
- Infrastructure domains: DNS servers, mail servers, etc.
- Record types other than A/AAAA: TXT, MX, NS, DS records are usually consistent
You should expect different results for:
- CDN-backed websites: Major websites, cloud services, media platforms
- Geographically distributed services: Global services with regional presence
- Load-balanced applications: Services with multiple server endpoints
DNS query response times can vary significantly:
- Local/ISP DNS: 1-50ms (but may have outdated records)
- Public DNS providers: 10-200ms (usually more up-to-date)
- International queries: 100-500ms (depending on geographic distance)
DnsClientX implements intelligent timeout and retry logic:
- Default timeout: 2000ms (2 seconds) - optimized for fast responses
- Automatic retry: Failed queries are retried with exponential backoff
- Provider fallback: Can automatically switch between providers
- Protocol fallback: UDP → TCP → HTTPS/TLS as needed
When testing DNS resolution:
- Use stable domains for consistency tests (e.g.,
google.com,github.com) - Use CDN domains to test geographic/provider differences
- Test multiple record types (A, AAAA, TXT, MX, NS, DS)
- Allow for reasonable response time variation (50-500ms)
- Validate structure, not exact content for CDN domains
- ✅ Normal for CDN domains - indicates proper geographic optimization
⚠️ Investigate for non-CDN domains - may indicate DNS propagation issues
- Check network connectivity to the DNS provider
- Consider using geographically closer DNS servers
- Verify firewall/proxy settings aren't interfering
- Enable retry logic and exponential backoff
- Test with multiple DNS providers
- Check for rate limiting or blocking
This behavior is by design and reflects the modern, distributed nature of internet infrastructure. DnsClientX provides tools to work effectively with this reality while maintaining reliable DNS resolution.
DnsEndpoint.System and DnsEndpoint.SystemTcp query the DNS servers exposed by the operating system. Discovery returns an immutable SystemDnsConfiguration containing the ordered server list, search suffixes, ndots, discovery source, effective Windows NRPT rules, and separate resolver/policy discovery errors.
Parameterless new ClientX(), ClientXBuilder, the static ClientX.QueryDns methods, DnsQueryOptions, the CLI, and Resolve-DnsQuery use the operating system's DNS resolver by default. Find-DnsService and Get-DnsService also use this default through parameterless clients. System DNS uses UDP with TCP fallback and the discovered system DNS policy; it does not automatically fall back to public resolvers when no OS resolver is available. In that case, configure an explicit endpoint or opt in with new ClientX(systemDnsFallback: SystemDnsFallback.PublicResolvers).
To retain Cloudflare JSON-over-HTTPS behavior, use new ClientX(DnsEndpoint.Cloudflare) or new ClientXBuilder().WithEndpoint(DnsEndpoint.Cloudflare).Build(). This changes the default of newly compiled constructor calls; C# callers compiled against an older optional argument retain their embedded endpoint until rebuilt. The builder uses the default in the loaded library. Select an endpoint explicitly when DNS transport or query destination must remain stable across upgrades.
- Windows and other .NET platforms enumerate every active interface. Interfaces are ordered by the native Windows interface metric when available, then by gateway presence and a stable interface order.
- Unix-like systems can fall back to
/etc/resolv.conf, includingnameserver,search/domain, andoptions ndots:n. - Configured loopback and link-local resolvers are preserved because local forwarding stubs and IPv6 scoped DNS servers are valid. Unspecified and multicast addresses are rejected.
- UDP clients reuse healthy connected sockets and automatically retry a truncated response over TCP when
UseTcpFallbackis enabled. TCP and DoT clients reuse persistent RFC 7766 connections, pipeline bounded concurrent queries, and dispatch out-of-order responses by transaction ID. - On Windows, dependency-free NRPT discovery honors the Group Policy-over-local-policy precedence rule. Supported suffix, prefix, FQDN, and catch-all matches route through
GenericDNSServers;DNSSECValidationRequiredtriggers local chain validation. Punycode IDN policy is supported, while Windows-only UTF-8 IDN modes, conflicts, DirectAccess, IPsec, wildcard/subnet, and auto-trigger VPN behavior fail explicitly instead of bypassing policy. - There is no silent public-resolver substitution. Missing system DNS is an explicit configuration error unless the caller opts in to
SystemDnsFallback.PublicResolvers.
Call client.EndpointConfiguration.RefreshSystemDns() after a network or VPN change to replace a system endpoint's resolver and policy snapshot. Refresh is explicit; existing clients do not subscribe to operating-system network events. Native scoped resolver behavior beyond the discovered configuration still depends on the host platform.
using var systemClient = new ClientX(new Configuration(DnsEndpoint.System));
var response = await systemClient.Resolve("intranet", DnsRecordType.A);
// Explicit opt-in for applications that prefer availability over preserving
// the local DNS trust and routing boundary.
var configuration = new Configuration(
DnsEndpoint.System,
DnsSelectionStrategy.First,
SystemDnsFallback.PublicResolvers);
using var fallbackClient = new ClientX(configuration);
var discovered = SystemInformation.GetDnsConfiguration(refresh: true);
Console.WriteLine($"Source: {discovered.Source}; ndots: {discovered.Ndots}");
Console.WriteLine(string.Join(", ", discovered.DnsServers));
Console.WriteLine(string.Join(", ", discovered.SearchDomains));
foreach (var rule in discovered.PolicyRules) {
Console.WriteLine($"NRPT {rule.Id}: {string.Join(", ", rule.Namespaces)} -> {string.Join(", ", rule.NameServers)}");
}Search expansion is enabled by default for system endpoints and can be disabled with Configuration.UseSystemSearchDomains. A trailing dot is always treated as absolute, and PTR inputs are never search-expanded.
On Linux, discovery reads the complete ordered search list from /etc/resolv.conf. The last search or domain directive replaces earlier entries, following the resolver configuration rules.
NRPT enforcement is enabled by default for system endpoints and can be deliberately disabled with Configuration.UseSystemDnsPolicies. The applied match is available as DnsResponse.AppliedSystemDnsPolicy. DnsClientX reads policy directly and does not emulate Windows DirectAccess, VPN activation, or IPsec state; use the operating-system resolver API when those services own the policy.
Persistent TCP/DoT reuse is enabled by default. Set EnableTcpConnectionReuse = false only for compatibility diagnostics with a broken server. MaxTcpQueriesPerConnection bounds in-flight queries on one connection and defaults to 128. The configured timeout covers capacity, connection, transaction-ID reservation, write, and response waits. Caller cancellation removes only that transaction; late responses are consumed without completing another query.
This direct DNS client does not delegate name resolution to the operating-system resolver service and does not claim parity with every platform-specific resolver feature. Configure a Configuration instance before issuing concurrent queries; each query takes a defensive snapshot, but concurrent mutation of shared configuration is not a supported control plane.
The public roadmap shows what is being worked on now, next and under exploration. Implemented capabilities, their qualification limits and the remaining engineering work are tracked in PLAN.md; optional encrypted transports are described in OPTIONAL-PROTOCOLS.md.
Important
Protocol coverage and RFC conformance are still maturing, and the public API can change between releases. Issues and pull requests are welcome, especially interoperability reports from real resolvers and authoritative servers.
DnsClientX provides multiple ways to query DNS records with extensive customization options.
using DnsClientX;// Simple A record lookup using Cloudflare
var response = await ClientX.QueryDns("google.com", DnsRecordType.A, DnsEndpoint.Cloudflare);
foreach (var answer in response.Answers) {
Console.WriteLine($"{answer.Name} -> {answer.Data}");
}// Use system-configured DNS servers
var response = await ClientX.QueryDns("google.com", DnsRecordType.A, DnsEndpoint.System);
response.Answers.DisplayToConsole();using var client = new ClientX(DnsEndpoint.Cloudflare);
var recordTypes = new[] { DnsRecordType.A, DnsRecordType.AAAA, DnsRecordType.MX };
var responses = await client.Resolve("google.com", recordTypes);
responses.DisplayTable();Keep resolver configuration on the client and query behavior in DnsResolveOptions:
using var client = new ClientX(DnsEndpoint.Cloudflare);
client.EndpointConfiguration.MaxConcurrency = 8;
var options = new DnsResolveOptions { TypedRecords = true, ValidateDnsSec = true };
await foreach (var response in client.ResolveStreaming(
new[] { "example.com", "example.net" }, new[] { DnsRecordType.A, DnsRecordType.AAAA }, options)) {
foreach (var record in response.GetTypedAnswers<ARecord>())
Console.WriteLine($"{record.Name}: {record.Value.Address} TTL={record.TTL}");
}Resolve(name, type, options) and Resolve(names, types, options) share this behavior. ResolveStreaming defaults to input order; set preserveOrder: false for completion order. ResolveAsyncEnumerable also yields in completion order. These streams admit at most MaxConcurrency queries, or 32 when unset, and cancel and drain admitted work when the consumer stops. ResolveStream retains its sequential behavior. Static ClientX.QueryDnsStreaming(request) accepts the same ResolveDnsRequest as QueryDns, reuses clients, and streams in record type then name order by default.
Locally accepted DNSSEC material is reused by the same client and revalidated for each response. Reuse expires at the earliest material TTL or authenticated proof deadline. Bogus and indeterminate validations cannot populate that cache. Set DnsSecMaterialCacheEnabled = false to disable it, or call ClearDnsSecMaterialCache(). Iterative root traversal keeps its separate validation session.
Import-Module DnsClientX
'example.com', 'example.net' | Resolve-Dns -Type A -Stream
[pscustomobject]@{ Domain = 'example.com' } | Resolve-Dns -Type MX -TypedRecords -TypedRecordMetadata
Resolve-Dns example.com -FullResponse | Get-DnsQueryExplanation
Find-DnsService example.com -Service http -Protocol tcp
Get-DnsService example.com -Service http -Resolve -ResolveHosts
Invoke-DnsUpdate -Server 192.0.2.53 -Zone example.com -Name host.example.com -Type A -Data 192.0.2.10 -WhatIfResolve-Dns emits one answer row per record. -TypedRecords emits parsed payloads; -TypedRecordMetadata adds owner and TTL metadata. -FullResponse emits response envelopes, including status and diagnostics when no answers exist. Names bind by value or through Name, Domain, and DnsName properties. -Stream preserves input order; -CompletionOrder opts into completion order. DNS service discovery emits individual rows, supports explicit resolvers, and observes pipeline cancellation. Probe, benchmark and health commands warn on failed policy by default; add -FailOnPolicyViolation -ErrorAction Stop to enforce a gate in automation.
using var client = new ClientXBuilder()
.WithEndpoint(DnsEndpoint.Cloudflare)
.WithTimeout(5000)
.WithUserAgent("MyApp/1.0")
.Build();
var response = await client.Resolve("example.com", maxRetries: 3);using var client = new ClientX(DnsEndpoint.Cloudflare,
userAgent: "MyApp/1.0",
httpVersion: new Version(2, 0),
maxConnectionsPerServer: 10);On .NET 8 or later, IP-literal HTTPS endpoints use the specified HTTP/1.1 or HTTP/2 version without automatic HTTP/3 upgrades. Select DnsRequestFormat.DnsOverHttp3 or an explicit HTTP version of 3.0 to request HTTP/3. Hostname endpoints retain HTTP version negotiation.
using var client = new ClientXBuilder()
.WithBaseUri(new Uri("https://1.1.1.1/dns-query"), DnsRequestFormat.DnsOverHttpsJSON)
.Build();Custom endpoints should be provided at construction time (builder/overloads) so the underlying HTTP client is configured correctly.
You can also preconfigure a Configuration and pass it into ClientX:
var config = new Configuration(new Uri("https://1.1.1.1/dns-query"), DnsRequestFormat.DnsOverHttpsJSON);
using var client = new ClientX(config);When resolving arrays of names with the helper methods, you can optionally cap in-flight requests:
using var client = new ClientX(DnsEndpoint.Cloudflare);
client.EndpointConfiguration.MaxConcurrency = Environment.ProcessorCount; // or any positive integer
// Caps parallelism for array-based helpers like Resolve/ResolveFilter
var responses = await client.Resolve(new[] { "example.com", "google.com" }, DnsRecordType.A);By default (null), DnsClientX keeps existing behavior and does not impose an explicit concurrency cap.
DnsClientX includes a flexible multi-resolver that can query multiple endpoints using different strategies.
- Strategies: FirstSuccess, FastestWins, SequentialFallback, RoundRobin
- Transports: UDP, TCP, DoT, DoH, DoH3, and DoQ where supported by the target runtime and endpoint
- Behaviors: per-query timeout, cancellation propagation, UDP→TCP fallback on truncation, TTL metrics
Usage example:
var endpoints = EndpointParser.TryParseMany(new []{
"1.1.1.1:53",
"8.8.8.8:53",
"https://dns.google/dns-query"
}, out var errors);
var options = new MultiResolverOptions {
Strategy = MultiResolverStrategy.FirstSuccess,
MaxParallelism = 4,
RespectEndpointTimeout = true
};
using var mr = new DnsMultiResolver(endpoints, options);
var response = await mr.QueryAsync("example.com", DnsRecordType.A);
Console.WriteLine($"RTT: {response.RoundTripTime.TotalMilliseconds} ms via {response.UsedTransport} @ {response.UsedEndpoint}");
// Batch API (preserves input order, isolates failures)
var results = await mr.QueryBatchAsync(new [] { "a.com", "b.com", "c.com" }, DnsRecordType.A);
// Consensus/diagnostic API (one response per endpoint, in endpoint order)
var allResponses = await mr.QueryAllAsync("example.com", DnsRecordType.DNSKEY);Notes:
- DoH endpoints must be provided as HTTPS URLs (e.g. https://dns.google/dns-query).
- When a UDP response is truncated and
AllowTcpFallbackis true, TCP is used automatically. Response.TtlMinandResponse.TtlAvgexpose TTL metrics derived from the answer set.
- FirstSuccess: Races a bounded set of endpoints and returns the first success. Cancels losers.
- FastestWins: Warms endpoints and prefers the one that produced the fastest successful response, caching the choice for a duration.
- SequentialFallback: Tries endpoints in order; returns the first terminal response or best transport error.
- RoundRobin: Distributes queries across endpoints to balance load. On failure, falls back to the first (or second) endpoint. Combine with MaxParallelism (global cap) and PerEndpointMaxInFlight (per-endpoint cap).
MaxParallelism: Caps total in-flight queries issued by the multi-resolver.PerEndpointMaxInFlight: Caps concurrent queries per endpoint (e.g., limit to 4 per DNS server).
- Enable TTL-aware response caching (off by default in multi-resolver) to avoid repeat lookups.
- C#:
var opts = new MultiResolverOptions {
EnableResponseCache = true,
MaxCacheTtl = TimeSpan.FromMinutes(60)
};Positive answers use the lowest answer TTL, including aliases. Negative NoError/NXDomain answers require an SOA and use the RFC 2308 minimum of the SOA TTL and SOA MINIMUM field, bounded further by aliases and denial proofs. Cached answer, authority, and additional TTLs decrease with elapsed time; original wire bytes remain receipt-time evidence. MaxCacheTtl can shorten a lifetime. Responses without a usable lifetime are not cached.
The response cache is shared by cache-enabled clients in the process. ClientX.GetResponseCacheStatistics(), ClientX.ResponseCacheCapacity, ClientX.ClearResponseCache(), and ClientX.InvalidateResponseCache(name, type) expose its management. Entries remain separated by resolver, transport and DNSSEC policy. Statistics count internal lookups, including single-flight checks; they are not query counts. PowerShell exposes Get-DnsCache and Clear-DnsCache [-Name example.com] [-Type A]. The reusable ResolveDnsRequest and Resolve-Dns cache options work with single and multiple resolvers.
- PowerShell:
Resolve-Dns -Name 'example.com' -Type A -DnsProvider Cloudflare,Google `
-ResolverStrategy FirstSuccess -ResponseCache -MaxCacheTtlSeconds 3600Balance across providers with caps:
Resolve-Dns -Name @('a.com','b.com') -Type A `
-DnsProvider System,Cloudflare,Quad9 `
-ResolverStrategy RoundRobin -MaxParallelism 32 -PerEndpointMaxInFlight 4First success across mixed endpoints:
Resolve-Dns -Name 'example.com' -Type A `
-ResolverEndpoint '1.1.1.1:53','https://dns.google/dns-query' -ResolverStrategy FirstSuccessEDNS client subnet and DNSSEC details:
Resolve-Dns -Name 'example.com' -Type A `
-ResolverEndpoint 'https://dns.google/dns-query' `
-EnableEdns -ClientSubnet '192.0.2.0/24' -RequestDnsSec -ValidateDnsSec -FullResponse- Clear for all:
Clear-DnsMultiResolverCache- Clear for specific providers or endpoints:
Clear-DnsMultiResolverCache -ResolverDnsProvider Cloudflare,Google
Clear-DnsMultiResolverCache -ResolverEndpoint '1.1.1.1:53','https://dns.google/dns-query'DnsClientX can automatically parse DNS records into strongly-typed objects, making it easier to work with structured data like SPF, DMARC, DKIM, and other complex record types.
using var client = new ClientX(DnsEndpoint.Cloudflare);
// Get typed records instead of raw strings
var response = await client.Resolve("google.com", DnsRecordType.MX, typedRecords: true);
// Access strongly-typed properties
foreach (var typedAnswer in response.TypedAnswers!) {
if (typedAnswer is MxRecord mx) {
Console.WriteLine($"Mail Server: {mx.Exchange}, Priority: {mx.Preference}");
}
}Use metadata-preserving projections when the owner, TTL, class or raw RDATA matters. They parse the current records, so a cache hit carries its remaining TTL:
foreach (var record in response.GetTypedAnswers<MxRecord>()) {
Console.WriteLine($"{record.Name} TTL={record.TTL}: {record.Value.Exchange}");
}GetTypedAuthorities<TRecord>() and GetTypedAdditional<TRecord>() provide the same envelope for other sections. GetTypedAnswers<object>() includes every parsed payload, including UnknownRecord for malformed or unsupported data. Existing TypedAnswers payload arrays remain available.
Available Typed Record Classes:
| Record Type | Typed Class | Key Properties |
|---|---|---|
| A | ARecord |
Address (IPAddress) |
| AAAA | AAAARecord |
Address (IPAddress) |
| MX | MxRecord |
Exchange, Preference |
| SRV | SrvRecord |
Target, Port, Priority, Weight |
| TXT | TxtRecord, SpfRecord, DmarcRecord, DkimRecord |
Parsed content |
| CAA | CaaRecord |
Flags, Tag, Value |
| TLSA | TlsaRecord |
CertificateUsage, Selector, MatchingType, AssociationData |
| SOA | SoaRecord |
PrimaryNameServer, ResponsibleEmail, Serial, etc. |
| CNAME | CNameRecord |
Target |
| NS | NsRecord |
NameServer |
| PTR | PtrRecord |
DomainName |
| DNSKEY | DnsKeyRecord |
Flags, Protocol, Algorithm, PublicKey |
| DS | DsRecord |
KeyTag, Algorithm, DigestType, Digest |
| NAPTR | NaptrRecord |
Order, Preference, Flags, Services, Regexp, Replacement |
| LOC | LocRecord |
Latitude, Longitude, Altitude, Size, etc. |
| SVCB, HTTPS | SvcbRecord |
Priority, Target, Alpn, Port, address hints, ECH, Parameters |
With typedRecords: true, TypedAnswers, TypedAuthorities, and TypedAdditional use the same parsers and TXT parsing option. SVCB and HTTPS share an immutable model and a canonical presentation across wire and JSON responses. Unknown service parameters retain their numeric keys and exact byte values in Parameters.
AliasMode records retain their parameters as evidence while their service views remain empty. Registered service parameters use their defined wire encodings, including TLS supported groups, CoAP paths, provisioning-domain flags, and transport weights. Zone-file parsing preserves quoted values and escapes.
Serializing a SvcbRecord stores Priority, Target, and Parameters; deserializing restores its computed ALPN, port, address hints, and ECH views from those parameter bytes.
var bindings = await client.Resolve("cloudflare.com", DnsRecordType.HTTPS, typedRecords: true);
foreach (var record in bindings.TypedAnswers!.OfType<SvcbRecord>()) {
Console.WriteLine($"{record.Priority} {record.Target}: {string.Join(", ", record.Alpn)}");
}DataRaw retains the received text and Data supplies the normalized presentation. Wire responses also expose the record's Class, RawRdata, and SourceWireMessage, plus the response's RawWireMessage. Byte getters return independent copies. RDATA can contain compression pointers, so retain its source message when interpreting those bytes. JSON responses leave unavailable class and wire evidence null; the library does not infer bytes that the provider did not supply.
Multicast DNS separates CacheFlush and the question's UnicastResponseRequested flag from Class. The source wire message retains the received bits; unicast responses and OPT payload sizes keep their original fields.
Specialized TXT Record Parsing:
using var client = new ClientX(DnsEndpoint.Cloudflare);
// SPF Records
var spfResponse = await client.Resolve("google.com", DnsRecordType.TXT, typedRecords: true, parseTypedTxtRecords: true);
foreach (var answer in spfResponse.TypedAnswers!) {
if (answer is SpfRecord spf) {
Console.WriteLine($"SPF Mechanisms: {string.Join(", ", spf.Mechanisms)}");
}
}
// DMARC Records
var dmarcResponse = await client.Resolve("_dmarc.google.com", DnsRecordType.TXT, typedRecords: true, parseTypedTxtRecords: true);
foreach (var answer in dmarcResponse.TypedAnswers!) {
if (answer is DmarcRecord dmarc) {
Console.WriteLine($"DMARC Policy: {dmarc.Tags["p"]}");
Console.WriteLine($"DMARC Percentage: {dmarc.Tags.GetValueOrDefault("pct", "100")}");
}
}
// DKIM Records
var dkimResponse = await client.Resolve("default._domainkey.google.com", DnsRecordType.TXT, typedRecords: true, parseTypedTxtRecords: true);
foreach (var answer in dkimResponse.TypedAnswers!) {
if (answer is DkimRecord dkim) {
Console.WriteLine($"DKIM Key Type: {dkim.Tags.GetValueOrDefault("k", "rsa")}");
Console.WriteLine($"DKIM Public Key: {dkim.Tags["p"]}");
}
}Working with Service Records:
using var client = new ClientX(DnsEndpoint.Cloudflare);
// SRV Records for service discovery
var srvResponse = await client.Resolve("_sip._tcp.example.com", DnsRecordType.SRV, typedRecords: true);
foreach (var answer in srvResponse.TypedAnswers!) {
if (answer is SrvRecord srv) {
Console.WriteLine($"Service: {srv.Target}:{srv.Port}");
Console.WriteLine($"Priority: {srv.Priority}, Weight: {srv.Weight}");
}
}
// TLSA Records for certificate validation
var tlsaResponse = await client.Resolve("_443._tcp.example.com", DnsRecordType.TLSA, typedRecords: true);
foreach (var answer in tlsaResponse.TypedAnswers!) {
if (answer is TlsaRecord tlsa) {
Console.WriteLine($"Certificate Usage: {tlsa.CertificateUsage}");
Console.WriteLine($"Selector: {tlsa.Selector}");
Console.WriteLine($"Matching Type: {tlsa.MatchingType}");
Console.WriteLine($"Association Data: {tlsa.AssociationData}");
}
}Certificate Authority Authorization (CAA):
using var client = new ClientX(DnsEndpoint.Cloudflare);
var caaResponse = await client.Resolve("google.com", DnsRecordType.CAA, typedRecords: true);
foreach (var answer in caaResponse.TypedAnswers!) {
if (answer is CaaRecord caa) {
Console.WriteLine($"CAA Tag: {caa.Tag}");
Console.WriteLine($"CAA Value: {caa.Value}");
Console.WriteLine($"Critical: {(caa.Flags & 128) != 0}");
}
}Location Records:
using var client = new ClientX(DnsEndpoint.Cloudflare);
var locResponse = await client.Resolve("example.com", DnsRecordType.LOC, typedRecords: true);
foreach (var answer in locResponse.TypedAnswers!) {
if (answer is LocRecord loc) {
Console.WriteLine($"Location: {loc.Latitude:F6}, {loc.Longitude:F6}");
Console.WriteLine($"Altitude: {loc.Altitude}m");
Console.WriteLine($"Size: {loc.Size}m");
}
}Controlling TXT Record Parsing:
using var client = new ClientX(DnsEndpoint.Cloudflare);
// Force all TXT records to be returned as simple TxtRecord objects
// instead of parsing into specialized types like SpfRecord, DmarcRecord, etc.
var response = await client.Resolve("google.com", DnsRecordType.TXT,
typedRecords: true,
parseTypedTxtRecords: false);
foreach (var answer in response.TypedAnswers!) {
if (answer is TxtRecord txt) {
Console.WriteLine($"Combined TXT: {txt.Text}");
foreach (string fragment in txt.Strings) {
Console.WriteLine($"Decoded fragment: {fragment}");
}
Console.WriteLine($"Original presentation: {txt.RawText}");
}
}Mixed Record Type Handling:
using var client = new ClientX(DnsEndpoint.Cloudflare);
var responses = await client.Resolve("google.com",
new[] { DnsRecordType.A, DnsRecordType.MX, DnsRecordType.TXT },
typedRecords: true,
parseTypedTxtRecords: true);
foreach (var response in responses) {
foreach (var answer in response.TypedAnswers!) {
switch (answer) {
case ARecord a:
Console.WriteLine($"A Record: {a.Address}");
break;
case MxRecord mx:
Console.WriteLine($"MX Record: {mx.Exchange} (Priority: {mx.Preference})");
break;
case SpfRecord spf:
Console.WriteLine($"SPF Record: {string.Join(" ", spf.Mechanisms)}");
break;
case TxtRecord txt:
Console.WriteLine($"TXT Record: {txt.Text}");
break;
case UnknownRecord unknown:
Console.WriteLine($"Unknown Record: {unknown.Data}");
break;
}
}
}using var client = new ClientX(DnsEndpoint.Cloudflare);
var response = await client.Resolve("google.com", DnsRecordType.A,
requestDnsSec: true,
validateDnsSec: true);
Console.WriteLine($"Authentic Data (AD): {response.AuthenticData}");
Console.WriteLine($"Local DNSSEC status: {response.DnsSecValidationStatus}");
Console.WriteLine(response.DnsSecValidationMessage);The resolver-provided AuthenticData flag and local validation are deliberately separate. Secure means DnsClientX built and verified a chain to a configured root trust anchor; Insecure means a secure parent authenticated an unsigned delegation; Bogus is a cryptographic or proof failure; and Indeterminate means the response, trust state, or supported algorithms were insufficient. The dependency-free validator supports RSA/SHA-1 (algorithms 5 and 7, for compatibility), RSA/SHA-256, and RSA/SHA-512 on every target. The .NET 8 and .NET 10 assets also support ECDSA P-256/SHA-256 and ECDSA P-384/SHA-384. Ed25519 and Ed448 are supplied by the optional package below rather than claimed by the core.
The optional DnsClientX.DnsSec.EdDsa project supplies ECDSA P-256/P-384 on .NET Framework and .NET Standard, and Ed25519/Ed448 on any supported target. It is not published on NuGet; build it from source before using the example below. It uses BouncyCastle.Cryptography; the core package does not depend on it:
using DnsClientX.DnsSec.EdDsa;
using var client = new ClientX(DnsEndpoint.RootServer);
client.EndpointConfiguration.UseEdDsaDnsSec();
Console.WriteLine(string.Join(", ", client.EndpointConfiguration.SupportedDnsSecAlgorithms));
DnsResponse response = await client.Resolve("signed.example", DnsRecordType.A,
requestDnsSec: true, validateDnsSec: true);The extension verifier participates in the same chain validation, cache isolation, and Secure/Bogus/Indeterminate semantics as the built-in algorithms. Merely referencing the package does not enable it; configuration is explicit per client.
Configuration.SupportedDnsSecAlgorithms reports the algorithms available on the running target, including the configured verifier. Query options, resolver execution options, and multi-resolver options accept the same IDnsSecSignatureVerifier instance. A configured verifier takes precedence for the algorithms it supports; other algorithms use the built-in verifier.
CLI queries, probes and benchmarks accept --dnssec-verifier /path/to/DnsClientX.DnsSec.EdDsa.dll. PowerShell Resolve-Dns, Test-DnsProbe and Test-DnsBenchmark accept -DnsSecVerifierPath with the same local assembly path. Keep the matching target's provider assembly and BouncyCastle.Cryptography.dll together. Loading is explicit and executes trusted local code; see the optional provider documentation for examples.
DNSSEC wire queries advertise RFC 9824 compact-answer support through the EDNS CO flag. Configure client.EndpointConfiguration.EdnsOptions = new EdnsOptions { CompactAnswersOk = false } to disable that negotiation. DnsMessageOptions.CompactAnswersOk is an explicit opt-in for low-level message construction.
For authenticated compact denial, response.DnsSecCompactDenial is true and response.EffectiveStatus is NXDomain, even if the server returned NoError in its header. response.Status retains that observed header code. This normalization requires a locally verified NSEC or NSEC3 NXNAME proof and a secure trust chain; a resolver's AD or CO flag alone cannot establish it. Signed empty non-terminals remain NODATA, and the cache bounds the verdict by its authenticated dependencies without synthesizing name errors for descendants or other record types. NXNAME is a denial metatype and cannot be queried directly.
The root-server profile enables RFC 9156 QNAME minimization by default. It asks for one delegation label at a time with type NS, continues through authoritative NODATA responses without revealing the final name, and sends the complete name and requested type only after reaching the authoritative zone. DnsResponse.QNameMinimizedQueryCount and QNameMinimizationFallbackCount make both the privacy protection and any compatibility downgrade visible. Set Configuration.EnableQNameMinimization = false only for controlled compatibility diagnostics.
RFC 5011 trust-anchor maintenance is opt-in because durable state belongs to the application. Configure a private, durable file and schedule explicit refreshes:
using var client = new ClientX(DnsEndpoint.RootServer);
string appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
if (string.IsNullOrWhiteSpace(appData)) throw new InvalidOperationException("Choose a durable state directory.");
client.EndpointConfiguration.Rfc5011TrustAnchorStorePath =
Path.Combine(appData, "ExampleApp", "root-anchors.json");
DnsSecTrustAnchorRefreshResult refresh = await client.RefreshRootTrustAnchorsAsync();
if (!refresh.Succeeded) throw new InvalidOperationException(refresh.Response.DnsSecValidationMessage);
Console.WriteLine($"Refresh again by {refresh.Snapshot!.NextRefreshUtc:O}");The state file is a root of trust. Keep its directory durable and writable only by the application identity or its administrator. Use the file from one process at a time for both DNSSEC validation and refresh: lookups also update its state, and separate processes are not coordinated. A relative path is resolved when assigned, so use a stable absolute location across restarts.
The state machine enforces the 30-day add hold-down plus original DNSKEY TTL, requires a validated post-deadline observation, resets absent pending keys, preserves missing active keys, immediately and permanently honors a key's verified self-revocation, and retains revoked tombstones through the 30-day remove hold-down. State writes replace a same-directory temporary file atomically; malformed state and clock rollback fail closed. No background timer is hidden inside ClientX: the application must call RefreshRootTrustAnchorsAsync by NextRefreshUtc and retry failed refreshes according to its scheduler. Without a configured store, the immutable IANA public-key anchors bundled with the package remain the validation boundary.
Private signed zones can start their trust chain from an explicitly configured DS anchor:
var configuration = new Configuration("192.0.2.53", DnsRequestFormat.DnsOverUDP) {
DnsSecTrustAnchors = new[] {
new DnsSecZoneTrustAnchor("corp.example", new DnsSecTrustAnchor(
trustedKeyTag, DnsKeyAlgorithm.ECDSAP256SHA256, 2, trustedSha256Digest))
}
};
using var client = new ClientX(configuration);
var response = await client.Resolve("host.corp.example", DnsRecordType.A,
requestDnsSec: true, validateDnsSec: true);
Console.WriteLine(response.DnsSecValidationStatus);Obtain the key tag, algorithm, and digest through the zone owner's trusted provisioning channel. The configured apex is an explicit trust boundary: its DNSKEY RRset must match the DS and verify its signatures; descendant delegations still require authenticated DS/DNSKEY proofs. The most specific configured zone applies. Invalid, expired, unsupported, or mismatched anchor material cannot be bypassed through the public parent chain, and cache identities and authenticated lifetimes include the configured anchors. These DNSSEC anchors are separate from TlsTrustAnchors, which authenticate encrypted transport certificates.
using var client = new ClientX(DnsEndpoint.Cloudflare);
// Query server1.example.com, server2.example.com, server3.example.com
var responses = await client.ResolvePattern("server[1-3].example.com", DnsRecordType.A);
responses.DisplayTable();using var client = new ClientX(DnsEndpoint.Cloudflare);
// Find TXT records containing "v=spf1"
var response = await client.ResolveFilter("google.com", DnsRecordType.TXT, "v=spf1");
response?.DisplayTable();var providers = new[] {
DnsEndpoint.Cloudflare,
DnsEndpoint.Google,
DnsEndpoint.Quad9
};
var endpoints = DnsResolverEndpointFactory.From(providers);
using var resolver = new DnsMultiResolver(endpoints, new MultiResolverOptions {
Strategy = MultiResolverStrategy.FirstSuccess,
MaxParallelism = 3
});
var response = await resolver.QueryAsync("google.com", DnsRecordType.A);
response.DisplayTable();using var client = new ClientX(DnsEndpoint.Cloudflare);
var domains = new[] { "google.com", "github.com", "microsoft.com" };
var recordTypes = new[] { DnsRecordType.A, DnsRecordType.AAAA };
await foreach (var response in client.ResolveStream(domains, recordTypes)) {
var question = response.Questions.Length > 0 ? response.Questions[0] : null;
Console.WriteLine(question is null
? "Resolved response"
: $"Resolved: {question.Name} ({question.Type})");
response.DisplayTable();
}// JSON format
var response = await ClientX.QueryDns("google.com", DnsRecordType.A,
DnsEndpoint.Cloudflare);
// Wire format
var response = await ClientX.QueryDns("google.com", DnsRecordType.A,
DnsEndpoint.CloudflareWireFormat);Browser/WASM and dependency-injected hosts can keep ownership of their HTTP handler while reusing DnsClientX parsing and response semantics:
var response = await DnsJsonQueryClient.QueryAsync(
httpClient,
new Uri("https://dns.google/resolve"),
"google.com",
DnsRecordType.A,
requestDnsSec: true,
cancellationToken: cancellationToken);var response = await ClientX.QueryDns("google.com", DnsRecordType.A,
DnsEndpoint.Quad9Quic);// System DNS with UDP (fallback to TCP)
var response = await ClientX.QueryDns("google.com", DnsRecordType.A,
DnsEndpoint.System);
// Force TCP
var response = await ClientX.QueryDns("google.com", DnsRecordType.A,
DnsEndpoint.SystemTcp);For authoritative, CHAOS-class, and EDNS diagnostics, construct the message explicitly and keep the shared transport and validation rules:
var query = new DnsMessage("version.bind", DnsRecordType.TXT, new DnsMessageOptions(
RecursionDesired: false,
QueryClass: 3)); // CHAOS
var result = await DnsWireQueryClient.QueryUdpAsync("192.0.2.53", 53, query);
Console.WriteLine($"{result.Response.WireMessageLength} response bytes");DnsWireQueryClient validates the source peer, transaction ID, QR/opcode, and echoed question before returning. It preserves the exact request/response bytes for protocol diagnostics while avoiding a second DNS transport implementation in consumers.
var response = await ClientX.QueryDns("google.com", DnsRecordType.A,
DnsEndpoint.RootServer);RootServer performs non-recursive iteration from the configured A-M root hints. It follows the closest delegation, applies glue according to the responding authority's bailiwick, resolves missing name-server addresses independently, and follows bounded CNAME/DNAME chains. With validateDnsSec: true, validation fetches DNSKEY/DS material through the same iterative path and anchors it at the bundled root trust anchors.
The profile goes through the normal Resolve retry, cache, audit, answer-projection, typed-record, PTR, and IDN pipeline. Referral and alias traversal has its own bound, independent of retry attempts:
using var client = new ClientX(DnsEndpoint.RootServer);
client.EndpointConfiguration.IterativeMaxHops = 40;
var response = await client.Resolve("www.sidn.nl", validateDnsSec: true, typedRecords: true);When an alias crosses authoritative packets or zones, each wire response is validated separately. DnsClientX does not combine signatures and RDATA offsets from different DNS messages or claim that an incomplete alias chain is secure.
using var client = new ClientX(DnsEndpoint.Cloudflare);
var latency = await client.MeasureLatencyAsync();
Console.WriteLine($"Latency: {latency.TotalMilliseconds}ms");using var client = new ClientX(DnsEndpoint.Cloudflare, timeOutMilliseconds: 5000);
var response = await client.Resolve(
"example.com",
maxRetries: 3,
retryDelayMs: 100);var endpoints = new[] {
DnsEndpoint.Cloudflare,
DnsEndpoint.Google,
DnsEndpoint.System
};
DnsResponse? response = null;
foreach (var endpoint in endpoints) {
try {
using var client = new ClientX(endpoint);
response = await client.Resolve("google.com", DnsRecordType.A);
break; // Success, exit loop
} catch (Exception ex) {
Console.WriteLine($"Failed with {endpoint}: {ex.Message}");
// Continue to next endpoint
}
}DDR asks the currently selected unencrypted IP resolver which encrypted resolvers it designates. Inspect the candidates, then explicitly create a client from a verified designation:
using var original = new ClientX("1.1.1.1", DnsRequestFormat.DnsOverUDP);
var candidates = await original.DiscoverDesignatedResolversAsync();
foreach (var candidate in candidates) {
Console.WriteLine($"{candidate.Binding.Alpn} {candidate.ConnectionAddress}: " +
$"{candidate.Verification} {candidate.VerificationError}");
}
var designated = candidates.FirstOrDefault(candidate =>
candidate.Verification == DnsDesignatedResolverVerification.Verified);
if (designated is null) throw new InvalidOperationException("No verified encrypted resolver was discovered.");
using var encrypted = new ClientX(designated.CreateConfiguration());
var response = await encrypted.Resolve("example.com", DnsRecordType.A);DiscoverDesignatedResolversAsync queries _dns.resolver.arpa and keeps SVCB/CNAME aliases and address lookups pinned to the original resolver. Compatible bindings respect mandatory parameters, ALPN, per-protocol default ports, explicit ports, address hints, and UTF-8 DoH paths. DnsServiceBindingSelection.SelectDnsResolvers exposes compatibility selection separately for already acquired SVCB records; selection alone does not authenticate a resolver.
Verified discovery requires both a trusted certificate chain and an exact SAN IP match for the original resolver. A different connection address never replaces that identity or the DoH URI authority. Options bound alias depth, candidate count, and each verification connection's timeout. Verify = false returns inspection candidates marked NotAttempted; they cannot create an encrypted configuration. Failed or unsupported verification stays explicit, and discovery never changes the original client or falls back to cleartext. CreateConfiguration rejects expired designations. Repeat discovery after a network change, even if the next network uses the same resolver IP.
DoH path templates use the dependency-free DnsDohUriTemplate parser. It supports all current RFC 6570 operators, variable lists, and scalar prefix/explode modifiers. GET fills only dns with unpadded Base64url; POST fills no template variables and sends the message in its body. Unknown variables remain undefined. Malformed templates and paths that replace the origin or introduce fragments are rejected. Configuration.DohUriTemplate accepts an absolute HTTPS template for explicitly configured wire-format DoH endpoints.
Custom application TLS roots use DnsDesignatedResolverDiscoveryOptions.TrustAnchors and are retained by the returned encrypted configuration. Configuration.TlsTrustAnchors applies the same strict chain and SAN name validation to encrypted transports; it does not install certificates in a persistent store. Keep caller-owned root certificates alive with their clients. Isolated custom-root trust and HTTP/2 ALPN verification require modern .NET. QUIC verification also requires a host with usable QUIC support. HTTP/1.1 and HTTP/2 connections to a different address with the original URI identity require .NET 8 or later. HTTP/3 DDR candidates with a different connection address are marked Unsupported, because the HTTP/3 client cannot retain that routing and authority combination for GET or POST. Same-address HTTP/3 and alternate-address DoQ remain available when the runtime and host support QUIC; same-address DoH and DoT remain available on older targets.
using var client = new ClientX(DnsEndpoint.Cloudflare);
// RFC 6763 meta-query: discover the advertised service types first.
var serviceTypes = await client.DiscoverServiceTypesAsync("example.com");
// Browse PTR instances of one service type, then resolve instance SRV/TXT data.
var services = await client.BrowseServicesAsync("http", "tcp", "example.com");
foreach (var service in services) {
Console.WriteLine($"Instance: {service.InstanceName}");
Console.WriteLine($"Type: {service.ServiceType} -> {service.Target}:{service.Port}");
}
// Return SRV targets in RFC 2782 priority and weighted-random connection order.
var srvRecords = await client.ResolveServiceAsync("ldap", "tcp", "example.com", resolveHosts: true);
foreach (var srv in srvRecords) {
Console.WriteLine($"Server: {srv.Target}:{srv.Port} (Priority: {srv.Priority})");
if (srv.Addresses != null) {
Console.WriteLine($"IPs: {string.Join(", ", srv.Addresses)}");
}
}For ongoing local discovery, use a multicast client and dispose or cancel the enumeration when browsing ends:
using var multicast = new ClientX("224.0.0.251", DnsRequestFormat.Multicast);
using var stop = new CancellationTokenSource(TimeSpan.FromMinutes(1));
var bounds = new DnsServiceBrowseOptions {
MaximumRecords = 512,
MaximumServices = 128,
EventBufferCapacity = 32
};
await foreach (var change in multicast.BrowseServicesContinuousAsync(
"http", "tcp", "local", bounds, stop.Token)) {
Console.WriteLine($"{change.Change}: {change.Service.InstanceName} " +
$"{change.Service.Target}:{change.Service.Port}");
}BrowseServicesContinuousAsync keeps one receiver open for unsolicited announcements as well as query replies. It yields independent Added, Updated, Removed, and Expired snapshots with TXT metadata and target addresses. Record processing honors cache-flush and goodbye rescue intervals, TTL refresh queries, and known-answer suppression. The configured multicast interface and port apply throughout the session. Record/instance limits fail explicitly; the event queue applies backpressure, so a persistently slow consumer can lose UDP announcements. Early enumeration disposal and client disposal cancel and drain the receiver before returning.
PowerShell uses the same core sessions and configuration:
Import-Module DnsClientX
Get-DnsDesignatedResolver -Server 1.1.1.1
Receive-DnsService -Service http -Protocol tcp -Domain local
# Pass a verified designation's configuration to the ordinary query cmdlet.
$designation = Get-DnsDesignatedResolver -Server 1.1.1.1 |
Where-Object Verification -EQ Verified | Select-Object -First 1
if ($null -eq $designation) { throw 'No verified encrypted resolver was discovered.' }
Resolve-Dns -Name example.com -Configuration ($designation.CreateConfiguration())Stopping Receive-DnsService or taking a bounded prefix ends its underlying browse session. Watch-DnsService is an alias. Use -Configuration and -Options for an explicit interface, multicast endpoint, and cache/event bounds.
// Full zone transfer
using var client = new ClientX("127.0.0.1", DnsRequestFormat.DnsOverTCP) {
EndpointConfiguration = { Port = 5353 }
};
var zoneRecords = await client.ZoneTransferAsync("example.com");
foreach (var rrset in zoneRecords) {
Console.WriteLine($"Zone chunk {rrset.Index}: {string.Join(", ", rrset.Records)}");
}
// Streaming zone transfer
await foreach (var rrset in client.ZoneTransferStreamAsync("example.com")) {
Console.WriteLine($"Received: {string.Join(", ", rrset)}");
}
// Atomic RFC 1995 incremental transfer. The server can return no change,
// ordered delete/add deltas, or a complete AXFR-style fallback.
var changes = await client.IncrementalZoneTransferAsync("example.com", currentSerial: 2026072100);
// Validate an RFC 8976 SIMPLE-SCHEME SHA-384/SHA-512 digest over the complete AXFR.
var digest = ZoneDigestValidator.Validate("example.com", zoneRecords);
Console.WriteLine($"{digest.Status}: {digest.Message}");AXFR and IXFR accept only DnsOverTCP or DnsOverTLS endpoints and enforce configurable message, record, byte, and per-I/O time limits. Streaming AXFR retries only before exposing the first chunk, so a failed transfer cannot replay a duplicate prefix. IXFR is returned only after the complete serial chain is validated and is therefore safe for an application to apply atomically.
On .NET 8 or newer, DnsOverTLS zone transfers enforce RFC 9103 TLS 1.3 and the dot ALPN value. An IP endpoint requires TlsServerName; IgnoreCertificateErrors is rejected. Mutual TLS is available through ZoneTransferClientCertificate, and a narrowly scoped certificate callback can be supplied through ZoneTransferServerCertificateValidationCallback for private PKI or pinned lab endpoints. XFR-over-TLS is explicitly unsupported on older targets rather than downgraded to plaintext.
On macOS, XFR-over-TLS requires .NET 10 or newer and the Network.framework TLS client. Enable it before the process performs any TLS work:
AppContext.SetSwitch("System.Net.Security.UseNetworkFramework", true);DnsClientX does not change this process-wide setting on the host's behalf. Check DnsTransportCapabilities.SupportsZoneTransferOverTls before selecting XFR-over-TLS.
ZoneDigestValidator operates on canonical wire records preserved by ZoneTransferAsync; presentation-only ZoneTransferResult values are rejected. A matching digest establishes zone-data integrity, not origin authenticity. Authenticate the apex ZONEMD RRset separately with DNSSEC or use an authenticated transfer channel.
Set Configuration.TsigKey to authenticate AXFR or IXFR message chains. The query is signed, the first and final responses require a valid TSIG, and unsigned intermediate messages are buffered until a subsequent signature authenticates them. Missing, invalid, or excessively delayed signatures fail the transfer before unauthenticated records are exposed. This authentication also applies to AXFR-style IXFR fallback and recursive transfers. XFR-over-TLS retains its strict certificate and ALPN requirements when TSIG is configured.
var parsed = DnsZoneFileParser.ParseFile("example.zone", new DnsZoneFileParseOptions {
Origin = "example.com",
AllowIncludes = true
});
foreach (var diagnostic in parsed.Diagnostics) {
Console.WriteLine(diagnostic);
}
if (parsed.Success) {
foreach (var record in parsed.Records) Console.WriteLine(record);
}The parser supports owner inheritance, $ORIGIN, compound TTL values, multiline records, quoted/comment-aware tokens, scoped relative $INCLUDE, common $GENERATE forms, and RFC 3597 TYPE#### records. Includes are disabled by default; enabling them permits only link-free paths beneath the top-level zone directory (or IncludeRootDirectory). Rooted, out-of-root, symbolic-link, and reparse-point paths additionally require AllowUnsafeIncludePaths = true and should be used only with trusted zone files. Unsupported classes, excessive generation, overflow, and malformed input are reported as structured diagnostics instead of being silently reinterpreted. BindFileParser remains a compatibility adapter over this public parser.
var configuration = new Configuration("192.0.2.53", DnsRequestFormat.DnsOverTCP) {
TsigKey = TsigKey.FromBase64("update-key.example.com", updateSecretFromSecureConfiguration)
};
using var client = new ClientX(configuration);
// Add a record
await client.UpdateRecordAsync(
"example.com", "www.example.com", DnsRecordType.A, "192.0.2.1", ttl: 300);
// Delete the complete A RRset
await client.DeleteRecordAsync("example.com", "www.example.com", DnsRecordType.A);
// Provision both records only if this name is still absent at the authoritative server.
var batch = new DnsUpdateBatch("example.com", new[] {
DnsUpdateOperation.Add("api.example.com", DnsRecordType.A, "192.0.2.20", ttl: 300),
DnsUpdateOperation.Add("api.example.com", DnsRecordType.TXT, "role=api", ttl: 300)
}, new[] { DnsUpdatePrerequisite.NameAbsent("api.example.com") });
await client.UpdateRecordsAsync(batch);DnsUpdateBatch sends its complete operation and prerequisite lists in one RFC 2136 message. The authoritative server evaluates name-existence, name-absence, RRset-existence, RRset-absence, or complete RRset equality before applying changes. Operations include additions, complete RRset deletion, individual-value deletion, and complete-name deletion. A failed prerequisite leaves the server's zone unchanged; the client does not split or partially replay a batch. The same TSIG request/response checks used for individual updates protect the complete transaction.
RFC 2136 wire updates require an explicit UDP/TCP-configured endpoint and are sent over TCP. Built-in profiles, including DnsEndpoint.System, DnsEndpoint.SystemTcp, and DnsEndpoint.RootServer, are rejected before sending an update. Create ClientX or Configuration with the authoritative server's hostname or IP address, or use ClientXBuilder.WithHostname with a UDP/TCP request format. To migrate from OS resolver targeting, choose the zone's authoritative update server explicitly; the operating system's configured resolver may only be a recursive server. DnsClientX does not discover the zone's UPDATE target or verify that the supplied endpoint is authoritative.
DoH, DoT, DoQ, and mDNS are not silently reinterpreted as plaintext update endpoints. When a TsigKey is configured, DnsClientX signs the request and requires a valid chained TSIG on the response. The separate JSON POST update mode is a proprietary custom-endpoint API and does not support TSIG.
using var client = new ClientX("224.0.0.251", DnsRequestFormat.Multicast) {
EndpointConfiguration = {
Port = 5353,
TimeOut = 1500,
MulticastInterfaceIndex = 12 // optional IPv6/IPv4 interface selection
}
};
var responses = await client.ResolveMulticastAllAsync("printer.local", DnsRecordType.A);The mDNS query uses transaction ID zero, clears RD, omits EDNS, and collects distinct valid responder messages for the configured timeout window. ResolveMulticastAllAsync is a bounded snapshot; BrowseServicesContinuousAsync maintains ongoing DNS-SD state and yields changes. Resolve is still available when a merged single response is more convenient.
var configuration = new Configuration("192.0.2.53", DnsRequestFormat.DnsOverUDP) {
LocalEndPoint = new IPEndPoint(IPAddress.Parse("192.0.2.10"), 0)
};
using var client = new ClientX(configuration);
DnsClientTelemetry.QueryCompleted += (_, query) =>
Console.WriteLine($"{query.Name} {query.Type}: {query.Status} in {query.Duration}");LocalEndPoint is supported for UDP, TCP, DoT, and DoQ queries and for RFC 2136 updates through the shared TCP engine. HTTP-based transports reject it explicitly because HttpClient does not expose a portable per-query source binding. Modern targets also emit ActivitySource spans and Meter counters under the name DnsClientX; when no event, activity, or meter listener is attached, query telemetry does not allocate a scope.
When response caching is enabled, DnsResponse.ResponseSource reports Network, Cache, or CoalescedNetwork. Concurrent misses on the same ClientX share one in-flight query when their answer identity and captured transport timeout match. Each caller receives an independent response clone. Live queries stay with their transport owner; completed cached answers can be reused by other clients. Canceling one waiter preserves surviving waiters. When the final waiter leaves, cancellation waits for transport cleanup before releasing the caller's admission slot.
Endpoint hostnames use system address resolution by default. Set an explicit bootstrap resolver to keep that lookup within a chosen DNS path:
using var client = new ClientXBuilder()
.WithEndpoint(DnsEndpoint.Google)
.WithBootstrapResolver(EndpointParser.ParseBootstrap("udp@1.1.1.1:53"))
.WithEdnsOptions(new EdnsOptions { RequestNsid = true })
.Build();
DnsResponse response = await client.Resolve("example.com");
Console.WriteLine(response.ServerResolution?.Address);
Console.WriteLine(response.EdnsNsidHex);Bootstrap accepts an IP-literal UDP/TCP resolver and does not fall back to system DNS after an explicit lookup failure. Queries, RFC 2136 updates, and zone transfers use the configured path. Successful addresses are cached for the shorter of their DNS TTL and the configured success TTL. ServerResolution reports the hostname, selected address, bootstrap path, cached or stale address use, and any lookup failure masked by stale reuse. Literal endpoints need no bootstrap lookup. HTTPS retains its original authority for HTTP Host, SNI, and certificate validation, including internationalized names. HTTP bootstrap requires .NET 8 or later and uses the specified HTTP/1.1 or HTTP/2 version without automatic HTTP/3 upgrades; HTTP/3, gRPC, iterative root resolution, multicast, and HTTP proxies reject it explicitly.
Certificate validation policy is captured when a query starts and remains in effect through retries. Changing IgnoreCertificateErrors affects subsequent queries. System clients can call EndpointConfiguration.RefreshSystemDns() after a network change. A refresh exposing no servers throws without replacing the prior configuration; pass SystemDnsFallback.PublicResolvers only when that fallback is desired.
NSID is an opaque EDNS value. EdnsNsid retains its bytes, EdnsNsidHex is lossless, and EdnsNsidText is present only for printable ASCII. JSON transports reject NSID requests because they cannot carry EDNS. The CLI accepts --bootstrap udp@1.1.1.1:53 --nsid; PowerShell query, probe, and benchmark cmdlets accept -BootstrapResolver 'udp@1.1.1.1:53' -RequestNsid. Use --explain or -FullResponse to inspect query provenance. JSON output includes server_resolution, edns_nsid_hex, and edns_nsid_text when available.
JSON requests on .NET 8 or later honor Configuration.HttpVersion. Set it to HTTP/1.1 when using an endpoint that supports only HTTP/1.1.
DnsClientX.Benchmarks contains BenchmarkDotNet suites for wire parsing, faithful TXT decoding, cache paths, and uncached UDP/TCP queries. The query comparison uses DnsClient.NET 1.8.0 (Apache-2.0), ARSoft.Tools.Net 3.6.1 (Apache-2.0), and DNS 7.0.0 (MIT) against the same controlled loopback responder with 1 and 16 A records. These dependencies belong only to the non-packable benchmark executable.
Run benchmarks one process at a time so BenchmarkDotNet build artifacts do not contend with each other:
# Discover or smoke-test the parser and cache suites.
dotnet run -c Release --project .\DnsClientX.Benchmarks -- --list flat
dotnet run -c Release --project .\DnsClientX.Benchmarks -- --filter '*DnsWireParserBenchmark*' --job Dry
dotnet run -c Release --project .\DnsClientX.Benchmarks -- --filter '*DnsResponseCacheBenchmark*' --job Dry
# Run the controlled loopback library comparison.
dotnet run -c Release --project .\DnsClientX.Benchmarks -- --filter '*DnsLibraryNetworkBenchmark*'To run the comparison against a resolver you control:
$env:DNS_BENCHMARK_SERVER = '192.0.2.53'
$env:DNS_BENCHMARK_PORT = '53'
$env:DNS_BENCHMARK_NAME = 'example.com'
$env:DNS_BENCHMARK_ANSWERS = '1' # Expected A record count supplied by the lab.
dotnet run -c Release --project .\DnsClientX.Benchmarks -- --filter '*DnsLibraryNetworkBenchmark*'The clients reuse their public client objects, disable response caching and outer retries, and use a two-second query deadline. DnsClientX enables TCP connection reuse. Each library retains its own public transport implementation and pooling behavior. DNS 7.0.0 has no TCP deadline setting, so its operation uses an external two-second wait. That package ships a non-optimized assembly; its measurements describe the published package, not an optimized source build. ARSoft.Tools.Net requires the .NET 8 ASP.NET Core runtime for this benchmark target.
Setup verifies both record count and address data before timing. The loopback responder runs in the benchmark process, so allocation totals include responder work and connection handling. Its TCP replies use complete frames with Nagle disabled, and completed successful connection tasks are reclaimed during the run. An external lab must return the configured answer count for every selected case. Treat latency and allocation figures as qualified measurements for the recorded runtime, host, CPU placement, and transport; they are not a portable ranking. Run competing lanes on the same CPU domain and keep performance measurements outside ordinary correctness CI.
Export BenchmarkDotNet JSON and use the shared PowerForge importer for normalized results:
dotnet run -c Release --project .\DnsClientX.Benchmarks -- --filter '*DnsLibraryNetworkBenchmark*' --exporters json --artifacts ./Ignore/Benchmarks
Import-Module PSPublishModule
Import-BenchmarkResult -Path ./Ignore/Benchmarks -Suite DnsClientX -OutputPath ./Ignore/Benchmarks/normalized.jsonFor a source baseline, set the MSBuild environment property DnsClientXSourceProject to the baseline checkout's DnsClientX.csproj, then run the same benchmark cases and options. A baseline that fails payload validation is an invalid lane; do not treat its timings as equivalent successful work.
For concurrent scenarios, use the dependency-free DnsClientX.LoadTests runner rather than BenchmarkDotNet. It reports throughput, p50/p95/p99 latency, and failures for each requested concurrency level and can emit JSON for comparisons over time:
dotnet run -c Release -f net10.0 --project .\DnsClientX.LoadTests -- `
--server 192.0.2.53 --name example.com --transport udp `
--concurrency 1,32,128 --requests 1000 --warmup 16 `
--json .\artifacts\dns-load.jsonUse a resolver you own for sustained tests. The runner disables caching and retries, creates one reusable client per concurrency scenario, and returns a nonzero exit code when any request fails. A NoError RCODE counts as success only when no response error is present and the complete pre-projection answer contains the requested RRset; missing or dropped answers are reported separately. Results compare complete query behavior; parser-only and cache-only costs belong in the microbenchmark suites above.
using var client = new ClientX(DnsEndpoint.Cloudflare) {
Debug = true // Enables detailed logging
};try {
using var client = new ClientX(DnsEndpoint.Cloudflare);
var response = await client.Resolve("nonexistent.domain", DnsRecordType.A);
if (!string.IsNullOrEmpty(response.Error)) {
Console.WriteLine($"DNS Error: {response.Error}");
} else if (!response.Answers.Any()) {
Console.WriteLine("No records found");
} else {
response.DisplayTable();
}
} catch (TimeoutException) {
Console.WriteLine("Query timed out");
} catch (Exception ex) {
Console.WriteLine($"Unexpected error: {ex.Message}");
}DnsClientX provides a comprehensive PowerShell module with multiple cmdlets for DNS operations.
Resolve-Dns covers the main query surface, including built-in providers, explicit server transports, resolver endpoint strategies, DNSSEC, EDNS/ECS, and typed records.
# Install from PowerShell Gallery
Install-Module -Name DnsClientX -Scope CurrentUser
# Import the module
Import-Module DnsClientX| Cmdlet | Alias | Description |
|---|---|---|
Resolve-Dns |
Resolve-DnsQuery |
Query DNS records with various providers |
Test-DnsBenchmark |
Benchmark multiple providers or endpoints | |
Get-DnsService |
Discover DNS services (DNS-SD) | |
Find-DnsService |
Find specific DNS services | |
Get-DnsZone |
Get-DnsZoneTransfer |
Perform DNS zone transfers |
Invoke-DnsUpdate |
Update DNS records (Dynamic DNS) |
# Simple A record lookup
Resolve-Dns -Name 'google.com' -Type A | Format-Table
# Query with specific DNS provider
Resolve-Dns -Name 'google.com' -Type A -DnsProvider Cloudflare | Format-Table
# Multiple domains at once
Resolve-Dns -Name 'google.com', 'github.com', 'microsoft.com' -Type A | Format-Table
# Multiple record types
Resolve-Dns -Name 'google.com' -Type A, AAAA, MX | Format-TableBenchmarks use Cold connections by default: each attempt owns a fresh client. Choose -ConnectionMode Warm to retain one client per resolver for the run, or --benchmark-connections warm in the CLI. Both modes send each attempt to the resolver with answer caching disabled. Client lifetime does not isolate operating-system caches or TLS session state.
Probe and benchmark rows expose CandidateId alongside the readable Target. Use CandidateId to correlate rows: endpoints with the same host and transport can differ in timeout, TLS identity, address family, request format, EDNS or DNSSEC settings. These variants are scored separately and can coexist in one health profile. CLI text includes candidate IDs in ranked results, recommendations and answer variants. Machine summary fields ending in _candidate_id percent-encode the ID; percent-decode the value to correlate it with JSON or saved scores. The identity describes the configured endpoint; shared run settings remain in the saved context and must also match for performance comparisons.
Saved scores use schema 3 and preserve candidate identities in results, answer observations and recommendations. Get-DnsResolverSelection and CLI --resolver-use reuse the saved endpoint definition. Schema 3 reuse requires its candidate ID and saved execution metadata; incomplete snapshots are rejected. Use ResolverProbeWorkflow or ResolverBenchmarkWorkflow to capture reusable selections; scorer-only snapshots do not capture a configured execution target. Schema 1/2 scores remain readable; their missing or ambiguous execution evidence cannot establish comparable performance. A selected raw target string is a display label and may omit endpoint settings; use the structured selection or saved score for reuse.
Latency covers the complete request, including first-use client setup, bootstrap lookups and DNSSEC validation. The core attempt result also exposes TransportElapsed when the primary transport execution is measured. This phase includes connection and transport-level address setup, and excludes HTTP bootstrap preparation and subsequent DNSSEC validation; cache hits and root iteration have no separate exchange metric. Reports and saved score summaries include ConnectionMode so results from different modes can be identified. Combining observations from different modes produces Mixed in reports; execution accepts only Cold or Warm. Maximum concurrency applies across the entire run.
# Benchmark built-in providers and return ranked candidate rows
Test-DnsBenchmark -Name 'example.com' -DnsProvider Cloudflare,Google,Quad9 -Attempts 3 |
Sort-Object Rank | Format-Table Target, SuccessPercent, AverageMs, Rank, IsRecommended
# Benchmark explicit resolver endpoints across domains and record types
Test-DnsBenchmark -Name 'example.com', 'microsoft.com' -Type A, AAAA `
-ResolverEndpoint 'udp@1.1.1.1:53', 'tcp@9.9.9.9:53' `
-Attempts 2 -MaxConcurrency 4 | Format-Table
# Add one run-level summary object after the candidate rows
Test-DnsBenchmark -Name 'example.com' -DnsProvider Cloudflare,Google `
-Attempts 3 -IncludeSummary
# Return only the run-level summary object for automation
Test-DnsBenchmark -Name 'example.com' -DnsProvider Cloudflare,Google `
-Attempts 3 -MinSuccessPercent 90 -SummaryOnly | Format-List
# Return only summary metadata that scripts can inspect directly
Test-DnsBenchmark -Name 'example.com' -DnsProvider Cloudflare,Google `
-Attempts 3 -MinSuccessPercent 90 -SummaryOnly |
Select-Object RecommendedTarget, RecommendationAvailable, OverallSuccessPercent, PolicyPassed# Use system-configured DNS servers
Resolve-Dns -Name 'google.com' -Type A -DnsProvider System -Verbose | Format-Table
# Query with custom DNS server over classic UDP
Resolve-Dns -Name 'google.com' -Type A -Server '8.8.8.8' | Format-Table
# Query a named resolver over DoH with explicit transport settings
Resolve-Dns -Name 'google.com' -Type A -Server 'dns.google' `
-RequestFormat DnsOverHttps -Port 443 -UserAgent 'DnsClientX/PowerShell' | Format-Table
# Multiple servers with fallback
Resolve-Dns -Name 'google.com' -Type A -Server '1.1.1.1', '8.8.8.8' -Fallback | Format-Table# Request and validate DNSSEC with a built-in provider
Resolve-Dns -Name 'google.com' -Type A -DnsProvider Cloudflare -RequestDnsSec -ValidateDnsSec | Format-Table
# Request and validate DNSSEC across explicit resolver endpoints
Resolve-Dns -Name 'google.com' -Type A `
-ResolverEndpoint 'https://dns.google/dns-query', 'tcp@9.9.9.9:53' `
-RequestDnsSec -ValidateDnsSec -FullResponse | Format-List
# Query DNSSEC-specific records
Resolve-Dns -Name 'google.com' -Type DS, DNSKEY -DnsProvider Cloudflare | Format-Table# Enable EDNS and send ECS (client subnet)
Resolve-Dns -Name 'example.com' -Type A -DnsProvider Quad9ECS `
-EnableEdns -ClientSubnet '192.0.2.0/24' | Format-Table
# Request resolver NSID metadata
Resolve-Dns -Name 'example.com' -Type A -ResolverEndpoint 'https://dns.google/dns-query' `
-EnableEdns -RequestNsid -FullResponse | Format-List# Query multiple servers using pattern
Resolve-Dns -Pattern 'server[1-3].example.com' -Type A -DnsProvider Cloudflare | Format-Table
# Range patterns
Resolve-Dns -Pattern 'host[10-15].example.com' -Type A | Format-Table# Get complete DNS response information
$Response = Resolve-Dns -Name 'google.com' -Type A -DnsProvider Cloudflare -FullResponse
# Display different parts of the response
$Response.Questions | Format-Table
$Response.Answers | Format-Table
$Response.AnswersMinimal | Format-Table
$Response.Authorities | Format-Table
$Response.Additional | Format-Table
# Check response metadata
Write-Host "Response Time: $([math]::Round($Response.RoundTripTime.TotalMilliseconds, 2))ms"
Write-Host "Server: $($Response.ServerAddress)"
Write-Host "Authentic Data (AD): $($Response.AuthenticData)"
Write-Host "DNSSEC Error: $(if([string]::IsNullOrEmpty($Response.Error)) { 'None' } else { $Response.Error })"# Custom timeout (in milliseconds)
Resolve-Dns -Name 'google.com' -Type A -TimeOut 5000 | Format-Table
# Limit per-client concurrency and HTTP connections
Resolve-Dns -Name 'google.com', 'microsoft.com' -Type A -DnsProvider Cloudflare `
-MaxConcurrency 8 -MaxConnectionsPerServer 20 | Format-Table
# With verbose output for troubleshooting
Resolve-Dns -Name 'google.com' -Type A -DnsProvider Cloudflare -Verbose | Format-Table# Standard Cloudflare (JSON)
Resolve-Dns -Name 'google.com' -Type A -DnsProvider Cloudflare | Format-Table
# Cloudflare Wire Format
Resolve-Dns -Name 'google.com' -Type A -DnsProvider CloudflareWireFormat | Format-Table
# Cloudflare Security (blocks malware)
Resolve-Dns -Name 'google.com' -Type A -DnsProvider CloudflareSecurity | Format-Table
# Cloudflare Family (blocks malware + adult content)
Resolve-Dns -Name 'google.com' -Type A -DnsProvider CloudflareFamily | Format-Table# Google DNS
Resolve-Dns -Name 'google.com' -Type A -DnsProvider Google | Format-Table
# Quad9 (security-focused)
Resolve-Dns -Name 'google.com' -Type A -DnsProvider Quad9 | Format-Table
# OpenDNS
Resolve-Dns -Name 'google.com' -Type A -DnsProvider OpenDNS | Format-Table
# AdGuard (ad-blocking)
Resolve-Dns -Name 'google.com' -Type A -DnsProvider AdGuard | Format-Table# MX records for email
Resolve-Dns -Name 'google.com' -Type MX | Format-Table
# SPF records (TXT records containing v=spf1)
Resolve-Dns -Name 'google.com' -Type TXT | Where-Object { $_.Data -like "*v=spf1*" } | Format-Table# TLSA records for certificate validation
Resolve-Dns -Name '_443._tcp.google.com' -Type TLSA | Format-Table
# CAA records for certificate authority authorization
Resolve-Dns -Name 'google.com' -Type CAA | Format-Table# SRV records for services
Resolve-Dns -Name '_sip._tcp.example.com' -Type SRV | Format-Table
# NAPTR records for service discovery
Resolve-Dns -Name 'example.com' -Type NAPTR | Format-Table# Discover all services in a domain
Get-DnsService -Domain 'example.com' | Format-Table
# Find services using the alternate cmdlet name
Find-DnsService -Domain 'example.com' | Where-Object ServiceName -like '*http*' | Format-Table# Full zone transfer (AXFR)
Get-DnsZone -Zone 'example.com' -Server '127.0.0.1' -Port 5353 | Format-Table
# Zone transfer with specific server
Get-DnsZoneTransfer -Zone 'example.com' -Server 'ns1.example.com' | Format-Table# Add a DNS record
Invoke-DnsUpdate -Zone 'example.com' -Server '127.0.0.1' -Name 'www' -Type A -Data '192.0.2.1' -Ttl 300
# Delete a DNS record
Invoke-DnsUpdate -Zone 'example.com' -Server '127.0.0.1' -Name 'www' -Type A -Delete
# Update an existing record
Invoke-DnsUpdate -Zone 'example.com' -Server '127.0.0.1' -Name 'www' -Type A -Data '192.0.2.2' -Ttl 600# Test connectivity to DNS servers
$Servers = @('1.1.1.1', '8.8.8.8', '9.9.9.9')
foreach ($Server in $Servers) {
try {
$Result = Resolve-Dns -Name 'google.com' -Type A -Server $Server -TimeOut 2000 -FullResponse
Write-Host "✓ $Server responded in $([math]::Round($Result.RoundTripTime.TotalMilliseconds, 2))ms" -ForegroundColor Green
} catch {
Write-Host "✗ $Server failed: $($_.Exception.Message)" -ForegroundColor Red
}
}# Enable verbose output for debugging
Resolve-Dns -Name 'google.com' -Type A -DnsProvider Cloudflare -Verbose
# Test with fallback servers
Resolve-Dns -Name 'google.com' -Type A -Server '1.1.1.1', '8.8.8.8' -Fallback -RandomServer -Verbose# Monitor website DNS resolution across providers
$Providers = @('Cloudflare', 'Google', 'Quad9', 'OpenDNS')
$Domain = 'example.com'
foreach ($Provider in $Providers) {
$Result = Resolve-Dns -Name $Domain -Type A -DnsProvider $Provider
Write-Host "$Provider -> $($Result.Data -join ', ')"
}# Complete email server DNS check
$Domain = 'example.com'
Write-Host "=== Email DNS Records for $Domain ===" -ForegroundColor Cyan
# MX Records
Write-Host "`nMX Records:" -ForegroundColor Yellow
Resolve-Dns -Name $Domain -Type MX | Format-Table Name, Priority, Data
# SPF Records
Write-Host "SPF Records:" -ForegroundColor Yellow
Resolve-Dns -Name $Domain -Type TXT | Where-Object { $_.Data -like "*v=spf1*" } | Format-Table Name, Data
# DMARC Records
Write-Host "DMARC Records:" -ForegroundColor Yellow
Resolve-Dns -Name "_dmarc.$Domain" -Type TXT | Format-Table Name, Data
# DKIM Records (example)
Write-Host "DKIM Records:" -ForegroundColor Yellow
Resolve-Dns -Name "default._domainkey.$Domain" -Type TXT | Format-Table Name, Data# DNS security assessment
$Domain = 'example.com'
Write-Host "=== DNS Security Assessment for $Domain ===" -ForegroundColor Cyan
# DNSSEC validation
$DnssecResult = Resolve-Dns -Name $Domain -Type A -DnsProvider Cloudflare -ValidateDnsSec -FullResponse
Write-Host "Authentic Data (AD): $($DnssecResult.AuthenticData)" -ForegroundColor $(if($DnssecResult.AuthenticData) { 'Green' } else { 'Yellow' })
Write-Host "Local DNSSEC status: $($DnssecResult.DnsSecValidationStatus)"
Write-Host "Validation details: $($DnssecResult.DnsSecValidationMessage)"
# CAA Records
Write-Host "`nCAA Records:" -ForegroundColor Yellow
Resolve-Dns -Name $Domain -Type CAA | Format-Table Name, Data
# Check for common security TXT records
Write-Host "Security Policy Records:" -ForegroundColor Yellow
Resolve-Dns -Name $Domain -Type TXT | Where-Object { $_.Data -match "(v=spf1|v=DMARC1|google-site-verification)" } | Format-Table Name, DataDnsClientX includes a command-line interface for quick DNS queries and scripting.
The CLI executable is DnsClientX.Cli.exe on Windows and DnsClientX.Cli on Linux and macOS. Build it from source with the commands below. Platform archives produced from Build/cli.publish.json contain the same executable and require a matching .NET 8 runtime. The Bash examples use DnsClientX.Cli; Windows users use DnsClientX.Cli.exe. From an extracted folder, prefix the name with ./ on Linux/macOS or .\ on Windows.
# Build from source
dotnet build DnsClientX.Cli/DnsClientX.Cli.csproj -c Release
# The executable will be available at:
# DnsClientX.Cli/bin/Release/net8.0/DnsClientX.Cli.exe (Windows)
# DnsClientX.Cli/bin/Release/net8.0/DnsClientX.Cli (Linux/macOS)
# Or build the entire solution
dotnet build DnsClientX.sln -c Release# Simple A record query using the default System endpoint
DnsClientX.Cli google.com
# Query with a specific built-in endpoint
DnsClientX.Cli --endpoint Cloudflare google.com
# Query a specific record type
DnsClientX.Cli --type MX google.com
# Explain which resolver/transport path was used
DnsClientX.Cli --endpoint Cloudflare --type A --explain google.com
# Include per-attempt diagnostics
DnsClientX.Cli --endpoint Cloudflare --type A --trace google.com# Probe a built-in endpoint family
DnsClientX.Cli --probe --endpoint Cloudflare example.com
# Probe custom endpoints
DnsClientX.Cli --probe --probe-endpoint udp@1.1.1.1:53,tcp@9.9.9.9:53 example.com
# Fail when responders disagree
DnsClientX.Cli --probe --probe-endpoint udp@1.1.1.1:53,tcp@9.9.9.9:53 \
--probe-require-consensus example.com
# Benchmark built-in endpoints across repeated queries
DnsClientX.Cli --benchmark --endpoint Cloudflare,Google,Quad9 \
--domain example.com --type A --benchmark-attempts 5
# Benchmark a custom endpoint matrix across domains and record types
DnsClientX.Cli --benchmark \
--probe-endpoint udp@1.1.1.1:53,tcp@9.9.9.9:53 \
--domain example.com,microsoft.com \
--type A,AAAA \
--benchmark-attempts 3 \
--benchmark-concurrency 4
# Add benchmark policy gates for automation
DnsClientX.Cli --benchmark --endpoint Cloudflare,Google,Quad9 \
--domain example.com \
--type A \
--benchmark-attempts 3 \
--benchmark-min-success-percent 90 \
--benchmark-min-successful-candidates 2 \
--benchmark-summary-line
# Keep CI logs compact while still emitting a machine-readable summary line
DnsClientX.Cli --benchmark --endpoint Cloudflare,Google,Quad9 \
--domain example.com \
--type A \
--benchmark-attempts 3 \
--benchmark-summary-only \
--benchmark-summary-line# Request and validate DNSSEC data
DnsClientX.Cli --endpoint Cloudflare --dnssec --validate-dnssec google.com
# Use DoH wire POST when supported
DnsClientX.Cli --endpoint CloudflareWireFormatPost --wire-post google.com
# Send a dynamic DNS update to an explicit authoritative server
DnsClientX.Cli --probe-endpoint tcp@192.0.2.53:53 \
--update example.com www A 192.0.2.10 --ttl 300#!/bin/bash
# Benchmark several providers and capture the one-line machine-readable summary
DnsClientX.Cli --benchmark --endpoint Cloudflare,Google,Quad9 \
--domain example.com \
--type A \
--benchmark-attempts 3 \
--benchmark-summary-line# PowerShell script for quick probe comparison
& DnsClientX.Cli.exe --probe `
--probe-endpoint 'udp@1.1.1.1:53,tcp@9.9.9.9:53' `
--probe-summary-line `
example.comRun the unified build from the repository root with PSPublishModule 3.0.158 or later installed:
# Refresh the PowerShell manifest without building or publishing packages.
./Module/Build/Build-Module.ps1 -RunMode Manifest -SignModule:$false
# Build the NuGet packages and PowerShell module without publishing.
./Module/Build/Build-Module.ps1 -RunMode Build
# Build, sign, and publish NuGet packages, the PowerShell module, and a GitHub release.
./Module/Build/Build-Module.ps1 -RunMode PublishThe release uses one three-part 2.1.x version for DnsClientX, the optional
DnsClientX.DnsSec.EdDsa package, and the PowerShell module. PowerForge resolves the
next package version, synchronizes it with the module's published versions, and
updates the PowerShell and CLI assembly versions. The packages build before the
module. Publication runs in order: NuGet, PowerShell Gallery, then GitHub.
GitHub uses the tag DnsClientX-v<version> and generates release notes from the
repository's changes. Package, module, and release artifacts are staged under
Artefacts/UploadReady; the project packages are under Artefacts/ProjectBuild.
Signing uses the certificate configured in the build files. For an unsigned local
module build, pass -SignModule:$false. NuGet signing is configured separately in
Build/project.build.json. The same file holds the NuGet API-key path; the module
entrypoint accepts -PowerShellGalleryApiKeyPath and -GitHubApiKeyPath for those
destinations. The GitHub key override applies to both package and module publishing;
relative GitHub key paths resolve from the current working directory. The default
paths refer to the maintainer's local credential files.
Only Publish mode reads publication credentials and uploads packages.
To inspect the package version plan without changing source files or publishing:
./Build/Build-Project.ps1 -Plan $trueBuild/Build-Project.ps1 remains available for package-only work. Use the module
entrypoint for a coordinated NuGet, PowerShell Gallery, and GitHub release.
Please consider sharing a post about DnsClientX and the value it provides. It really does help!
This project general idea is based on DnsOverHttps by @akac which was an inspiration for DnsClientX.
- DnsClient.NET - DnsClient is a simple yet very powerful and high performant open source library for the .NET Framework to do DNS lookups. If you need standard DNS support - this one is for you.
- DnsOverHttps - DnsOverHttps is a simple yet very powerful and high performant open source library for the .NET Framework to do DNS lookups over HTTPS using Cloudflare. If you only need Cloudflare support and target newer .NET versions - this one is for you.
- DinoDNS - another DNS library with a lot of features.