Skip to content

Implement DnsResolver for Windows#129302

Open
rzikm wants to merge 10 commits into
dotnet:mainfrom
rzikm:dns-resolver
Open

Implement DnsResolver for Windows#129302
rzikm wants to merge 10 commits into
dotnet:mainfrom
rzikm:dns-resolver

Conversation

@rzikm

@rzikm rzikm commented Jun 11, 2026

Copy link
Copy Markdown
Member

Implements the API approved in #19443:

  • New Dns.Resolve*[Async] static methods for A/AAAA/SRV/MX/TXT/CNAME/PTR/NS records.
  • New DnsResolver / DnsResolverOptions for instance-based resolution with optional custom DNS servers.
  • Record types AddressRecord, SrvRecord, MxRecord, TxtRecord, CNameRecord, PtrRecord, NsRecord.
  • DnsResponseCode enum and DnsResult envelope carrying ResponseCode, Records, and NegativeCacheTtl.

Windows implementation uses DnsQueryEx. Non-Windows platforms get PlatformNotSupportedException stubs pending follow-up implementations (splitting to multiple stacked PRs for easier review).

rzikm and others added 10 commits June 3, 2026 15:17
Implements the API approved in dotnet#19443:
* New Dns.Resolve*[Async] static methods for A/AAAA/SRV/MX/TXT/CNAME/PTR/NS records.
* New DnsResolver / DnsResolverOptions for instance-based resolution with optional custom DNS servers.
* Record types AddressRecord, SrvRecord, MxRecord, TxtRecord, CNameRecord, PtrRecord, NsRecord.
* DnsResponseCode enum and DnsResult<T> envelope carrying ResponseCode, Records, and NegativeCacheTtl.

Windows implementation uses DnsQueryEx (DNS_QUERY_REQUEST v1 by default; DNS_QUERY_REQUEST3 when the caller supplies non-default ports on Windows 11 build 22000+, throwing PlatformNotSupportedException otherwise). Non-Windows platforms get PlatformNotSupportedException stubs pending follow-up implementations.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds argument-validation tests and Windows-only OuterLoop network tests for the new DnsResolver / Dns.Resolve* APIs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… port 53

DnsQueryEx only contacts custom DNS servers on the standard port 53 and
requires the sockaddr port field to be 0; any non-zero port (even 53) is
rejected with ERROR_INVALID_PARAMETER. Simplify the Windows resolver to
always use the v1 DNS_QUERY_REQUEST path, write sockaddr port 0, and throw
PlatformNotSupportedException for server endpoints requesting a non-default
port. Remove the now-unused v3 custom-server code path.

Add an in-process loopback DNS server (bound to 127.0.0.1:53, skipped when
the port is unavailable) and a comprehensive behavioral test suite covering
address/SRV/MX/TXT/CNAME/PTR/NS resolution, NXDOMAIN vs NODATA, TTLs,
SRV additional-address glue, port-0 acceptance, and in-flight cancellation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The synchronous Resolve* methods previously blocked on the async path via
GetAwaiter().GetResult(). DnsQueryEx executes synchronously when no completion
callback is supplied, so call it directly instead of going through the async
state machine. Record-list parsing is factored into shared helpers reused by
both the sync and async paths, and the loopback behavioral tests are
parameterized over both APIs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move all platform-specific logic from DnsResolver.Windows.cs and
DnsResolver.WindowsAsync.cs into a new DnsResolverPal.Windows.cs static
PAL class, mirroring the existing NameResolutionPal pattern. The
cross-platform Resolve*Core methods remain on DnsResolver and delegate to
the PAL, providing a seam for future instrumentation and telemetry.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Instrument the Resolve*Core seam in DnsResolver with the existing
NameResolutionTelemetry infrastructure (EventSource counters, the
DnsLookup Activity span, and the dns.lookup.duration metric), matching
the static Dns class.

When no diagnostics consumer is enabled the PAL task is returned
directly, keeping the common path allocation-free and preserving the
synchronous-completion invariant the sync Resolve* methods depend on.

Extend NameResolutionActivity.Stop to accept a string[] answer so the
new record types can populate the dns.answers tag.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Resolve*Core methods invoked the PAL eagerly and only then wrapped
the resulting task with telemetry. On the synchronous path the Windows
PAL executes the query while creating the task, so BeforeResolution ran
after most of the work was done and the recorded duration excluded the
query time. Defer the PAL invocation behind a Func so telemetry brackets
the entire query for both sync and async paths.

Adds a regression test asserting the recorded dns.lookup.duration covers
a delayed server response on both sync and async paths.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 11, 2026 16:57
@rzikm rzikm requested a review from a team June 11, 2026 16:58
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @karelz, @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new DNS query API surface area to System.Net.NameResolution (new Dns.Resolve* static entry points plus an instance-based DnsResolver), and implements the Windows PAL using DnsQueryEx. It also introduces functional tests, including deterministic loopback-server-driven tests for record parsing/handling.

Changes:

  • Adds new public APIs: Dns.Resolve* (A/AAAA/SRV/MX/TXT/CNAME/PTR/NS), DnsResolver, DnsResolverOptions, record structs, DnsResult<T>, and DnsResponseCode.
  • Implements Windows DNS querying/parsing via DnsQueryEx (sync + async paths), plus non-Windows PNSE stubs.
  • Adds functional tests (outer-loop network tests + loopback DNS server tests) and extends NameResolution telemetry to handle string[] answers.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 18 comments.

Show a summary per file
File Description
src/libraries/System.Net.NameResolution/tests/FunctionalTests/System.Net.NameResolution.Functional.Tests.csproj Adds new functional test sources to the project
src/libraries/System.Net.NameResolution/tests/FunctionalTests/LoopbackDnsServer.cs Adds an in-process UDP/TCP loopback DNS server for deterministic tests
src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResponseBuilder.cs Adds a fluent builder for DNS response bytes used by loopback tests
src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs Adds outer-loop functional tests for the new APIs (real DNS)
src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs Adds deterministic loopback-server-driven tests for parsing/behavior/metrics
src/libraries/System.Net.NameResolution/src/System/Net/NameResolutionTelemetry.cs Extends telemetry answer handling to support string[]
src/libraries/System.Net.NameResolution/src/System/Net/DnsResult.cs Introduces DnsResult<T> envelope type
src/libraries/System.Net.NameResolution/src/System/Net/DnsResponseCode.cs Introduces DnsResponseCode enum
src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs Implements Windows PAL via DnsQueryEx, record parsing, cancellation, glue, TTL extraction
src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Unsupported.cs Adds non-Windows PNSE PAL stubs
src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverOptions.cs Adds options type for custom server selection
src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs Adds instance-based resolver API + telemetry wrapping
src/libraries/System.Net.NameResolution/src/System/Net/DnsRecords.cs Adds record structs (Address/Srv/Mx/Txt/CName/Ptr/Ns)
src/libraries/System.Net.NameResolution/src/System/Net/Dns.Resolve.cs Adds new static Dns.Resolve* APIs backed by a default resolver
src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs Makes Dns partial to host the new Dns.Resolve* partial
src/libraries/System.Net.NameResolution/src/System.Net.NameResolution.csproj Wires new sources and Windows Dnsapi interop into the build
src/libraries/System.Net.NameResolution/src/Resources/Strings.resx Adds SR string for unsupported custom DNS ports
src/libraries/System.Net.NameResolution/ref/System.Net.NameResolution.cs Adds new public API contract entries
src/libraries/Common/src/Interop/Windows/Interop.Libraries.cs Adds Libraries.Dnsapi constant
src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsTypes.cs Adds DnsQueryEx/DNS_RECORD-related structs/constants
src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsApi.cs Adds LibraryImport declarations for DnsQueryEx/DnsCancelQuery/DnsFree

Comment on lines +88 to +91
public System.Threading.Tasks.Task<System.Net.DnsResult<System.Net.CNameRecord>> ResolveCNameAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public System.Threading.Tasks.Task<System.Net.DnsResult<System.Net.PtrRecord>> ResolvePtrAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public System.Threading.Tasks.Task<System.Net.DnsResult<System.Net.PtrRecord>> ResolvePtrAsync(System.Net.IPAddress address, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public System.Threading.Tasks.Task<System.Net.DnsResult<System.Net.NsRecord>> ResolveNsAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
Comment on lines +504 to +510
// For NXDOMAIN/NODATA, try to extract the negative-cache TTL from the
// SOA in the authority section if it's present in the record list.
TimeSpan negativeTtl = TimeSpan.Zero;
if (rc != DnsResponseCode.NoError || records == IntPtr.Zero)
{
negativeTtl = ExtractNegativeCacheTtl(records);
}
Comment on lines +651 to +657
// For NXDOMAIN/NODATA, try to extract the negative-cache TTL from the
// SOA in the authority section if it's present in the record list.
TimeSpan negativeTtl = TimeSpan.Zero;
if (rc != DnsResponseCode.NoError || records == IntPtr.Zero)
{
negativeTtl = ExtractNegativeCacheTtl(records);
}
cur = hdr.pNext;
}

return new DnsResult<AddressRecord>(DnsResponseCode.NoError, records, TimeSpan.Zero);
cur = hdr.pNext;
}

return new DnsResult<SrvRecord>(DnsResponseCode.NoError, records, TimeSpan.Zero);
Comment on lines +215 to +216
Assert.Equal(DnsResponseCode.NoError, result.ResponseCode);
Assert.Empty(result.Records);
Comment on lines +196 to +197
Assert.Equal(DnsResponseCode.NxDomain, result.ResponseCode);
Assert.Empty(result.Records);
Comment on lines +4 to +6
using System;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
Comment on lines +32 to +36
public DnsResult<AddressRecord> ResolveAddresses(string name)
=> ResolveAddresses(name, AddressFamily.Unspecified);

public DnsResult<AddressRecord> ResolveAddresses(string name, AddressFamily addressFamily)
{

@MihaZupan MihaZupan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First pass through.

Just curious, will there be a way to opt-in to using the managed implementation even on Windows, or would there be no reason to want to do that anyway?

Comment on lines +75 to +82
public System.Net.DnsResult<System.Net.AddressRecord> ResolveAddresses(string name) { throw null; }
public System.Net.DnsResult<System.Net.AddressRecord> ResolveAddresses(string name, System.Net.Sockets.AddressFamily addressFamily) { throw null; }
public System.Net.DnsResult<System.Net.SrvRecord> ResolveSrv(string name) { throw null; }
public System.Net.DnsResult<System.Net.MxRecord> ResolveMx(string name) { throw null; }
public System.Net.DnsResult<System.Net.TxtRecord> ResolveTxt(string name) { throw null; }
public System.Net.DnsResult<System.Net.CNameRecord> ResolveCName(string name) { throw null; }
public System.Net.DnsResult<System.Net.PtrRecord> ResolvePtr(string name) { throw null; }
public System.Net.DnsResult<System.Net.NsRecord> ResolveNs(string name) { throw null; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public System.Net.DnsResult<System.Net.AddressRecord> ResolveAddresses(string name) { throw null; }
public System.Net.DnsResult<System.Net.AddressRecord> ResolveAddresses(string name, System.Net.Sockets.AddressFamily addressFamily) { throw null; }
public System.Net.DnsResult<System.Net.SrvRecord> ResolveSrv(string name) { throw null; }
public System.Net.DnsResult<System.Net.MxRecord> ResolveMx(string name) { throw null; }
public System.Net.DnsResult<System.Net.TxtRecord> ResolveTxt(string name) { throw null; }
public System.Net.DnsResult<System.Net.CNameRecord> ResolveCName(string name) { throw null; }
public System.Net.DnsResult<System.Net.PtrRecord> ResolvePtr(string name) { throw null; }
public System.Net.DnsResult<System.Net.NsRecord> ResolveNs(string name) { throw null; }

I think we should talk about this with the team :)

My stance continues to be that exposing sync APIs here is a mistake, especially if we have real plans to eventually add support for resolution over HTTPS/QUIC.
The fact that e.g. Resolve silently has 2x the latency compared to the async variant makes the argument that we need sync APIs for perf wrong IMO.

private static DnsResolver DefaultResolver =>
s_defaultResolver ??= new DnsResolver();

public static DnsResult<AddressRecord> ResolveAddresses(string name)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: missing /// comments on all the new members

Comment on lines +267 to +279
Span<char> chars = stackalloc char[32 * 2 + 9];
int pos = 0;
for (int i = 15; i >= 0; i--)
{
byte b = bytes[i];
chars[pos++] = ToHex(b & 0xF);
chars[pos++] = '.';
chars[pos++] = ToHex(b >> 4);
chars[pos++] = '.';
}
"ip6.arpa".AsSpan().CopyTo(chars.Slice(pos));
pos += "ip6.arpa".Length;
return new string(chars.Slice(0, pos));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Span<char> chars = stackalloc char[32 * 2 + 9];
int pos = 0;
for (int i = 15; i >= 0; i--)
{
byte b = bytes[i];
chars[pos++] = ToHex(b & 0xF);
chars[pos++] = '.';
chars[pos++] = ToHex(b >> 4);
chars[pos++] = '.';
}
"ip6.arpa".AsSpan().CopyTo(chars.Slice(pos));
pos += "ip6.arpa".Length;
return new string(chars.Slice(0, pos));
Span<char> chars = stackalloc char[16 * 4];
int pos = 0;
for (int i = bytes.Length - 1; i >= 0; i--)
{
byte b = bytes[i];
chars[pos++] = ToHex(b & 0xF);
chars[pos++] = '.';
chars[pos++] = ToHex(b >> 4);
chars[pos++] = '.';
}
return string.Concat(chars, "ip6.arpa");

{
Span<byte> bytes = stackalloc byte[4];
address.TryWriteBytes(bytes, out _);
return string.Create(System.Globalization.CultureInfo.InvariantCulture, $"{bytes[3]}.{bytes[2]}.{bytes[1]}.{bytes[0]}.in-addr.arpa");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return string.Create(System.Globalization.CultureInfo.InvariantCulture, $"{bytes[3]}.{bytes[2]}.{bytes[1]}.{bytes[0]}.in-addr.arpa");
return $"{bytes[3]}.{bytes[2]}.{bytes[1]}.{bytes[0]}.in-addr.arpa";

I don't think culture matters here since these are all positive values

? ResolveWithTelemetry(name, () => DnsResolverPal.ResolveNs(_options.Servers, async, name, cancellationToken), static r => MapAnswers(r, static a => a.Name))
: DnsResolverPal.ResolveNs(_options.Servers, async, name, cancellationToken);

private static async Task<DnsResult<T>> ResolveWithTelemetry<T>(string name, Func<Task<DnsResult<T>>> resolve, Func<DnsResult<T>, string[]> getAnswers)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider accepting TState here, right now a closure will be allocated for every call, even if telemetry is disabled


// Writes a SOCKADDR_IN or SOCKADDR_IN6 representation into the destination buffer.
// The buffer must be at least 28 bytes (sizeof sockaddr_in6).
private static unsafe void WriteSockAddr(byte* dest, IPEndPoint ep)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be better to accept an IPAddress argument here since that's all you're using, and the endpoint is technically mutable so we might be writing wrong bytes around

Comment on lines +711 to +716
Span<byte> addrBytes = stackalloc byte[4];
ep.Address.TryWriteBytes(addrBytes, out _);
dest[4] = addrBytes[0];
dest[5] = addrBytes[1];
dest[6] = addrBytes[2];
dest[7] = addrBytes[3];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Span<byte> addrBytes = stackalloc byte[4];
ep.Address.TryWriteBytes(addrBytes, out _);
dest[4] = addrBytes[0];
dest[5] = addrBytes[1];
dest[6] = addrBytes[2];
dest[7] = addrBytes[3];
ep.Address.TryWriteBytes(new Span<byte>(dest + 4, 4), out _);

Comment on lines +725 to +730
Span<byte> addrBytes = stackalloc byte[16];
ep.Address.TryWriteBytes(addrBytes, out _);
for (int i = 0; i < 16; i++)
{
dest[8 + i] = addrBytes[i];
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Span<byte> addrBytes = stackalloc byte[16];
ep.Address.TryWriteBytes(addrBytes, out _);
for (int i = 0; i < 16; i++)
{
dest[8 + i] = addrBytes[i];
}
ep.Address.TryWriteBytes(new Span<byte>(dest + 8, 16), out _);

Comment on lines +733 to +736
dest[24] = (byte)(scopeId & 0xff);
dest[25] = (byte)((scopeId >> 8) & 0xff);
dest[26] = (byte)((scopeId >> 16) & 0xff);
dest[27] = (byte)((scopeId >> 24) & 0xff);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
dest[24] = (byte)(scopeId & 0xff);
dest[25] = (byte)((scopeId >> 8) & 0xff);
dest[26] = (byte)((scopeId >> 16) & 0xff);
dest[27] = (byte)((scopeId >> 24) & 0xff);
BinaryPrimitives.WriteUInt32LittleEndian(new Span<byte>(dest + 24, 4), scopeId);

Is little endian right?

public DnsResolver(DnsResolverOptions options)
{
ArgumentNullException.ThrowIfNull(options);
_options = options;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider making a defensive copy of the servers list here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants