From 21787a2514953665f6cc90f4f284e9851a0f2c6b Mon Sep 17 00:00:00 2001 From: rzikm Date: Wed, 3 Jun 2026 15:17:50 +0200 Subject: [PATCH 01/19] Add System.Net.Dns resolver API surface and Windows implementation Implements the API approved in dotnet/runtime#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 (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> --- .../Interop/Windows/Dnsapi/Interop.DnsApi.cs | 71 +++ .../Windows/Dnsapi/Interop.DnsTypes.cs | 195 ++++++++ .../src/Interop/Windows/Interop.Libraries.cs | 1 + .../ref/System.Net.NameResolution.cs | 125 ++++++ .../src/Resources/Strings.resx | 3 + .../src/System.Net.NameResolution.csproj | 15 + .../src/System/Net/Dns.Resolve.cs | 73 +++ .../src/System/Net/Dns.cs | 2 +- .../src/System/Net/DnsRecords.cs | 116 +++++ .../src/System/Net/DnsResolver.Unsupported.cs | 35 ++ .../src/System/Net/DnsResolver.Windows.cs | 327 ++++++++++++++ .../System/Net/DnsResolver.WindowsAsync.cs | 419 ++++++++++++++++++ .../src/System/Net/DnsResolver.cs | 163 +++++++ .../src/System/Net/DnsResolverOptions.cs | 18 + .../src/System/Net/DnsResponseCode.cs | 23 + .../src/System/Net/DnsResult.cs | 40 ++ 16 files changed, 1625 insertions(+), 1 deletion(-) create mode 100644 src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsApi.cs create mode 100644 src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsTypes.cs create mode 100644 src/libraries/System.Net.NameResolution/src/System/Net/Dns.Resolve.cs create mode 100644 src/libraries/System.Net.NameResolution/src/System/Net/DnsRecords.cs create mode 100644 src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Unsupported.cs create mode 100644 src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Windows.cs create mode 100644 src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.WindowsAsync.cs create mode 100644 src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs create mode 100644 src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverOptions.cs create mode 100644 src/libraries/System.Net.NameResolution/src/System/Net/DnsResponseCode.cs create mode 100644 src/libraries/System.Net.NameResolution/src/System/Net/DnsResult.cs diff --git a/src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsApi.cs b/src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsApi.cs new file mode 100644 index 00000000000000..ce4e396ff9a8f3 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsApi.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +internal static partial class Interop +{ + internal static partial class Dnsapi + { + // ---- Query types we use ---- + internal const ushort DNS_TYPE_A = 0x0001; + internal const ushort DNS_TYPE_NS = 0x0002; + internal const ushort DNS_TYPE_CNAME = 0x0005; + internal const ushort DNS_TYPE_SOA = 0x0006; + internal const ushort DNS_TYPE_PTR = 0x000c; + internal const ushort DNS_TYPE_MX = 0x000f; + internal const ushort DNS_TYPE_TEXT = 0x0010; + internal const ushort DNS_TYPE_AAAA = 0x001c; + internal const ushort DNS_TYPE_SRV = 0x0021; + + // ---- DnsQueryEx return codes / Win32 error codes ---- + internal const int DNS_REQUEST_PENDING = 9506; + internal const int ERROR_SUCCESS = 0; + internal const int DNS_INFO_NO_RECORDS = 9501; + internal const int DNS_ERROR_RCODE_FORMAT_ERROR = 9001; + internal const int DNS_ERROR_RCODE_SERVER_FAILURE = 9002; + internal const int DNS_ERROR_RCODE_NAME_ERROR = 9003; + internal const int DNS_ERROR_RCODE_NOT_IMPLEMENTED = 9004; + internal const int DNS_ERROR_RCODE_REFUSED = 9005; + + // ---- DnsQueryEx options ---- + internal const ulong DNS_QUERY_STANDARD = 0x00000000; + internal const ulong DNS_QUERY_RETURN_MESSAGE = 0x00020000; + + // ---- Query request versions ---- + internal const uint DNS_QUERY_REQUEST_VERSION1 = 0x1; + internal const uint DNS_QUERY_REQUEST_VERSION3 = 0x3; + + // ---- DNS_ADDR address family marker — addresses are stored in SOCKADDR form ---- + internal const ushort AF_INET = 2; + internal const ushort AF_INET6 = 23; + + // ---- DNS_CUSTOM_SERVER server types ---- + internal const uint DNS_CUSTOM_SERVER_TYPE_UDP = 0x1; + internal const uint DNS_CUSTOM_SERVER_TYPE_DOH = 0x2; + + // ---- DNS_CUSTOM_SERVER usage flags ---- + internal const ulong DNS_CUSTOM_SERVER_UDP_FALLBACK = 0x1; + + // ---- DnsFreeType for DnsFree ---- + internal const int DnsFreeFlat = 0; + internal const int DnsFreeRecordList = 1; + internal const int DnsFreeParsedMessageFields = 2; + + internal delegate void DnsQueryCompletionRoutine(IntPtr pQueryContext, IntPtr pQueryResults); + + [LibraryImport(Libraries.Dnsapi, EntryPoint = "DnsQueryEx")] + internal static unsafe partial int DnsQueryEx( + DNS_QUERY_REQUEST* pQueryRequest, + DNS_QUERY_RESULT* pQueryResults, + DNS_QUERY_CANCEL* pCancelHandle); + + [LibraryImport(Libraries.Dnsapi, EntryPoint = "DnsCancelQuery")] + internal static unsafe partial int DnsCancelQuery(DNS_QUERY_CANCEL* pCancelHandle); + + [LibraryImport(Libraries.Dnsapi, EntryPoint = "DnsFree")] + internal static partial void DnsFree(IntPtr pData, int freeType); + } +} diff --git a/src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsTypes.cs b/src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsTypes.cs new file mode 100644 index 00000000000000..cf92e3e0307a29 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsTypes.cs @@ -0,0 +1,195 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Dnsapi + { + // DNS_QUERY_REQUEST (v1) — Win8 / Server 2012+ + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct DNS_QUERY_REQUEST + { + public uint Version; + public IntPtr QueryName; // PCWSTR + public ushort QueryType; + public ulong QueryOptions; + public DNS_ADDR_ARRAY* pDnsServerList; + public uint InterfaceIndex; + public IntPtr pQueryCompletionCallback; // PDNS_QUERY_COMPLETION_ROUTINE + public IntPtr pQueryContext; + } + + // DNS_QUERY_REQUEST3 — Win11 Build 22000+ + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct DNS_QUERY_REQUEST3 + { + public uint Version; + public IntPtr QueryName; + public ushort QueryType; + public ulong QueryOptions; + public DNS_ADDR_ARRAY* pDnsServerList; + public uint InterfaceIndex; + public IntPtr pQueryCompletionCallback; + public IntPtr pQueryContext; + public uint cCustomServers; + public DNS_CUSTOM_SERVER* pCustomServers; + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct DNS_QUERY_RESULT + { + public uint Version; + public int QueryStatus; + public ulong QueryOptions; + public IntPtr pQueryRecords; // DNS_RECORD* + public IntPtr Reserved; + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct DNS_QUERY_CANCEL + { + public fixed byte Reserved[32]; + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct DNS_ADDR + { + // SOCKET_ADDRESS-like: 32 bytes of SOCKADDR_STORAGE-ish + extras. + // DnsApi documents this struct as 64 bytes total with the first 32 + // being the SOCKADDR (IPv4/IPv6 SOCKADDR fits within). + public fixed byte MaxSa[32]; + public uint DnsAddrUserDword0; + public uint DnsAddrUserDword1; + public uint DnsAddrUserDword2; + public uint DnsAddrUserDword3; + public uint DnsAddrUserDword4; + public uint DnsAddrUserDword5; + public uint DnsAddrUserDword6; + public uint DnsAddrUserDword7; + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct DNS_ADDR_ARRAY + { + public uint MaxCount; + public uint AddrCount; + public uint Tag; + public ushort Family; + public ushort WordReserved; + public uint Flags; + public uint MatchFlag; + public uint Reserved1; + public uint Reserved2; + // followed by AddrCount entries of DNS_ADDR + // (we allocate the trailing array contiguously) + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct DNS_CUSTOM_SERVER + { + public uint dwServerType; // DNS_CUSTOM_SERVER_TYPE_* + public ulong ullFlags; + public IntPtr pwszTemplate; // PCWSTR (DoH only) + public fixed byte ServerAddr[32]; // SOCKADDR + } + + // ---- DNS_RECORD (variable layout: header + Data union) ---- + // We declare the fixed header layout and read the data area as a byte blob, + // re-interpreting per record type. The Data field begins at offset 24 on 32-bit + // and at offset 24 on 64-bit pointer layouts as documented; we use a + // Sequential struct with explicit Next/pName pointers. + [StructLayout(LayoutKind.Sequential)] + internal struct DNS_RECORD_HEADER + { + public IntPtr pNext; // DNS_RECORD* + public IntPtr pName; // PCWSTR + public ushort wType; + public ushort wDataLength; // not always reliable; use type to interpret + public uint Flags; // contains Section in the low bits + public uint dwTtl; + public uint dwReserved; + // followed by Data union + } + + // ---- Section field within DNS_RECORD.Flags ---- + // The Section is the lowest 2 bits of the DW_FLAGS field. + internal const uint DNSREC_SECTION_MASK = 0x3; + internal const uint DNSREC_QUESTION = 0; + internal const uint DNSREC_ANSWER = 1; + internal const uint DNSREC_AUTHORITY = 2; + internal const uint DNSREC_ADDITIONAL = 3; + + // ---- Data unions ---- + [StructLayout(LayoutKind.Sequential)] + internal struct DNS_A_DATA + { + public uint IpAddress; // network byte order + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct DNS_AAAA_DATA + { + public fixed byte Ip6Address[16]; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct DNS_PTR_DATA + { + public IntPtr pNameHost; // PCWSTR + } + + // Same shape as DNS_PTR_DATA — Windows uses DNS_PTR_DATA for NS/CNAME too, + // but typed aliases keep call sites self-documenting. +#pragma warning disable CS0649 // fields populated via native marshalling + internal struct DNS_CNAME_DATA + { + public IntPtr pNameHost; + } + + internal struct DNS_NS_DATA + { + public IntPtr pNameHost; + } +#pragma warning restore CS0649 + + [StructLayout(LayoutKind.Sequential)] + internal struct DNS_MX_DATA + { + public IntPtr pNameExchange; // PCWSTR + public ushort wPreference; + public ushort Pad; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct DNS_SRV_DATA + { + public IntPtr pNameTarget; // PCWSTR + public ushort wPriority; + public ushort wWeight; + public ushort wPort; + public ushort Pad; + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct DNS_TXT_DATA + { + public uint dwStringCount; + // followed by dwStringCount entries of PCWSTR + } + + [StructLayout(LayoutKind.Sequential)] + internal struct DNS_SOA_DATA + { + public IntPtr pNamePrimaryServer; // PCWSTR + public IntPtr pNameAdministrator; // PCWSTR + public uint dwSerialNo; + public uint dwRefresh; + public uint dwRetry; + public uint dwExpire; + public uint dwDefaultTtl; + } + } +} diff --git a/src/libraries/Common/src/Interop/Windows/Interop.Libraries.cs b/src/libraries/Common/src/Interop/Windows/Interop.Libraries.cs index a9a3a2fe167edc..af66c1f796edc0 100644 --- a/src/libraries/Common/src/Interop/Windows/Interop.Libraries.cs +++ b/src/libraries/Common/src/Interop/Windows/Interop.Libraries.cs @@ -12,6 +12,7 @@ internal static partial class Libraries internal const string Credui = "credui.dll"; internal const string Crypt32 = "crypt32.dll"; internal const string CryptUI = "cryptui.dll"; + internal const string Dnsapi = "dnsapi.dll"; internal const string Dsrole = "dsrole.dll"; internal const string Gdi32 = "gdi32.dll"; internal const string HttpApi = "httpapi.dll"; diff --git a/src/libraries/System.Net.NameResolution/ref/System.Net.NameResolution.cs b/src/libraries/System.Net.NameResolution/ref/System.Net.NameResolution.cs index 9a35c4275aa8df..a77da0600bc3fb 100644 --- a/src/libraries/System.Net.NameResolution/ref/System.Net.NameResolution.cs +++ b/src/libraries/System.Net.NameResolution/ref/System.Net.NameResolution.cs @@ -42,6 +42,24 @@ public static partial class Dns public static string GetHostName() { throw null; } [System.ObsoleteAttribute("Resolve has been deprecated. Use GetHostEntry instead.")] public static System.Net.IPHostEntry Resolve(string hostName) { throw null; } + public static System.Net.DnsResult ResolveAddresses(string name) { throw null; } + public static System.Net.DnsResult ResolveAddresses(string name, System.Net.Sockets.AddressFamily addressFamily) { throw null; } + public static System.Threading.Tasks.Task> ResolveAddressesAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task> ResolveAddressesAsync(string name, System.Net.Sockets.AddressFamily addressFamily, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Net.DnsResult ResolveSrv(string name) { throw null; } + public static System.Threading.Tasks.Task> ResolveSrvAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Net.DnsResult ResolveMx(string name) { throw null; } + public static System.Threading.Tasks.Task> ResolveMxAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Net.DnsResult ResolveTxt(string name) { throw null; } + public static System.Threading.Tasks.Task> ResolveTxtAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Net.DnsResult ResolveCName(string name) { throw null; } + public static System.Threading.Tasks.Task> ResolveCNameAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Net.DnsResult ResolvePtr(string name) { throw null; } + public static System.Net.DnsResult ResolvePtr(System.Net.IPAddress address) { throw null; } + public static System.Threading.Tasks.Task> ResolvePtrAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task> ResolvePtrAsync(System.Net.IPAddress address, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Net.DnsResult ResolveNs(string name) { throw null; } + public static System.Threading.Tasks.Task> ResolveNsAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } public partial class IPHostEntry { @@ -50,4 +68,111 @@ public IPHostEntry() { } public string[] Aliases { get { throw null; } set { } } public string HostName { get { throw null; } set { } } } + public sealed partial class DnsResolver : System.IAsyncDisposable, System.IDisposable + { + public DnsResolver() { } + public DnsResolver(System.Net.DnsResolverOptions options) { } + public System.Net.DnsResult ResolveAddresses(string name) { throw null; } + public System.Net.DnsResult ResolveAddresses(string name, System.Net.Sockets.AddressFamily addressFamily) { throw null; } + public System.Net.DnsResult ResolveSrv(string name) { throw null; } + public System.Net.DnsResult ResolveMx(string name) { throw null; } + public System.Net.DnsResult ResolveTxt(string name) { throw null; } + public System.Net.DnsResult ResolveCName(string name) { throw null; } + public System.Net.DnsResult ResolvePtr(string name) { throw null; } + public System.Net.DnsResult ResolveNs(string name) { throw null; } + public System.Threading.Tasks.Task> ResolveAddressesAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public System.Threading.Tasks.Task> ResolveAddressesAsync(string name, System.Net.Sockets.AddressFamily addressFamily, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public System.Threading.Tasks.Task> ResolveSrvAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public System.Threading.Tasks.Task> ResolveMxAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public System.Threading.Tasks.Task> ResolveTxtAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public System.Threading.Tasks.Task> ResolveCNameAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public System.Threading.Tasks.Task> ResolvePtrAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public System.Threading.Tasks.Task> ResolvePtrAsync(System.Net.IPAddress address, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public System.Threading.Tasks.Task> ResolveNsAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public void Dispose() { } + public System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } + } + public sealed partial class DnsResolverOptions + { + public DnsResolverOptions() { } + public System.Collections.Generic.IList Servers { get { throw null; } set { } } + } + public readonly partial struct DnsResult + { + private readonly T _dummyT; + private readonly object _dummy; + private readonly int _dummyPrimitive; + [System.CLSCompliantAttribute(false)] + public System.Net.DnsResponseCode ResponseCode { get { throw null; } } + public System.Collections.Generic.IReadOnlyList Records { get { throw null; } } + public System.TimeSpan NegativeCacheTtl { get { throw null; } } + } + [System.CLSCompliantAttribute(false)] + public enum DnsResponseCode : ushort + { + NoError = 0, + FormatError = 1, + ServerFailure = 2, + NxDomain = 3, + NotImplemented = 4, + Refused = 5, + } + public readonly partial struct AddressRecord + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public System.Net.IPAddress Address { get { throw null; } } + public System.TimeSpan Ttl { get { throw null; } } + } + public readonly partial struct SrvRecord + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public string Target { get { throw null; } } + [System.CLSCompliantAttribute(false)] + public ushort Port { get { throw null; } } + [System.CLSCompliantAttribute(false)] + public ushort Priority { get { throw null; } } + [System.CLSCompliantAttribute(false)] + public ushort Weight { get { throw null; } } + public System.TimeSpan Ttl { get { throw null; } } + public System.Collections.Generic.IReadOnlyList Addresses { get { throw null; } } + } + public readonly partial struct MxRecord + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public string Exchange { get { throw null; } } + [System.CLSCompliantAttribute(false)] + public ushort Preference { get { throw null; } } + public System.TimeSpan Ttl { get { throw null; } } + } + public readonly partial struct TxtRecord + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public System.Collections.Generic.IReadOnlyList Values { get { throw null; } } + public System.TimeSpan Ttl { get { throw null; } } + } + public readonly partial struct CNameRecord + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public string CanonicalName { get { throw null; } } + public System.TimeSpan Ttl { get { throw null; } } + } + public readonly partial struct PtrRecord + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public string Name { get { throw null; } } + public System.TimeSpan Ttl { get { throw null; } } + } + public readonly partial struct NsRecord + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public string Name { get { throw null; } } + public System.TimeSpan Ttl { get { throw null; } } + } } diff --git a/src/libraries/System.Net.NameResolution/src/Resources/Strings.resx b/src/libraries/System.Net.NameResolution/src/Resources/Strings.resx index ed124526d86fbe..de6f24bdacdd75 100644 --- a/src/libraries/System.Net.NameResolution/src/Resources/Strings.resx +++ b/src/libraries/System.Net.NameResolution/src/Resources/Strings.resx @@ -75,4 +75,7 @@ System.Net.NameResolution is not supported on this platform. + + Specifying a DNS server port other than 53 requires Windows 11 Build 22000 or later. + \ No newline at end of file diff --git a/src/libraries/System.Net.NameResolution/src/System.Net.NameResolution.csproj b/src/libraries/System.Net.NameResolution/src/System.Net.NameResolution.csproj index 47439bf44a3f63..89da5be9e94bf0 100644 --- a/src/libraries/System.Net.NameResolution/src/System.Net.NameResolution.csproj +++ b/src/libraries/System.Net.NameResolution/src/System.Net.NameResolution.csproj @@ -15,6 +15,12 @@ + + + + + + @@ -37,6 +43,13 @@ + + + + + @@ -76,6 +89,7 @@ + + + s_defaultResolver ??= new DnsResolver(); + + public static DnsResult ResolveAddresses(string name) + => DefaultResolver.ResolveAddresses(name); + + public static DnsResult ResolveAddresses(string name, AddressFamily addressFamily) + => DefaultResolver.ResolveAddresses(name, addressFamily); + + public static Task> ResolveAddressesAsync(string name, CancellationToken cancellationToken = default) + => DefaultResolver.ResolveAddressesAsync(name, cancellationToken); + + public static Task> ResolveAddressesAsync(string name, AddressFamily addressFamily, CancellationToken cancellationToken = default) + => DefaultResolver.ResolveAddressesAsync(name, addressFamily, cancellationToken); + + public static DnsResult ResolveSrv(string name) + => DefaultResolver.ResolveSrv(name); + + public static Task> ResolveSrvAsync(string name, CancellationToken cancellationToken = default) + => DefaultResolver.ResolveSrvAsync(name, cancellationToken); + + public static DnsResult ResolveMx(string name) + => DefaultResolver.ResolveMx(name); + + public static Task> ResolveMxAsync(string name, CancellationToken cancellationToken = default) + => DefaultResolver.ResolveMxAsync(name, cancellationToken); + + public static DnsResult ResolveTxt(string name) + => DefaultResolver.ResolveTxt(name); + + public static Task> ResolveTxtAsync(string name, CancellationToken cancellationToken = default) + => DefaultResolver.ResolveTxtAsync(name, cancellationToken); + + public static DnsResult ResolveCName(string name) + => DefaultResolver.ResolveCName(name); + + public static Task> ResolveCNameAsync(string name, CancellationToken cancellationToken = default) + => DefaultResolver.ResolveCNameAsync(name, cancellationToken); + + public static DnsResult ResolvePtr(string name) + => DefaultResolver.ResolvePtr(name); + + public static DnsResult ResolvePtr(IPAddress address) + => DefaultResolver.ResolvePtrAsync(address).GetAwaiter().GetResult(); + + public static Task> ResolvePtrAsync(string name, CancellationToken cancellationToken = default) + => DefaultResolver.ResolvePtrAsync(name, cancellationToken); + + public static Task> ResolvePtrAsync(IPAddress address, CancellationToken cancellationToken = default) + => DefaultResolver.ResolvePtrAsync(address, cancellationToken); + + public static DnsResult ResolveNs(string name) + => DefaultResolver.ResolveNs(name); + + public static Task> ResolveNsAsync(string name, CancellationToken cancellationToken = default) + => DefaultResolver.ResolveNsAsync(name, cancellationToken); + } +} diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs b/src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs index fdac620bbf15df..a46d27f0d2eac2 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs @@ -13,7 +13,7 @@ namespace System.Net { /// Provides simple domain name resolution functionality. - public static class Dns + public static partial class Dns { /// Gets the host name of the local machine. public static string GetHostName() diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsRecords.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsRecords.cs new file mode 100644 index 00000000000000..dd31abed2b1e27 --- /dev/null +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsRecords.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace System.Net +{ + /// An A or AAAA record resolved from DNS, with TTL. + public readonly struct AddressRecord + { + public IPAddress Address { get; } + public TimeSpan Ttl { get; } + + internal AddressRecord(IPAddress address, TimeSpan ttl) + { + Address = address; + Ttl = ttl; + } + } + + /// An SRV record (RFC 2782) with optional inlined address records from the additional section. + public readonly struct SrvRecord + { + private readonly IReadOnlyList? _addresses; + + public string Target { get; } + [CLSCompliant(false)] + public ushort Port { get; } + [CLSCompliant(false)] + public ushort Priority { get; } + [CLSCompliant(false)] + public ushort Weight { get; } + public TimeSpan Ttl { get; } + public IReadOnlyList Addresses => _addresses ?? Array.Empty(); + + internal SrvRecord(string target, ushort port, ushort priority, ushort weight, TimeSpan ttl, IReadOnlyList? addresses) + { + Target = target; + Port = port; + Priority = priority; + Weight = weight; + Ttl = ttl; + _addresses = addresses; + } + } + + /// An MX record (RFC 1035 §3.3.9). + public readonly struct MxRecord + { + public string Exchange { get; } + [CLSCompliant(false)] + public ushort Preference { get; } + public TimeSpan Ttl { get; } + + internal MxRecord(string exchange, ushort preference, TimeSpan ttl) + { + Exchange = exchange; + Preference = preference; + Ttl = ttl; + } + } + + /// A TXT record (RFC 1035 §3.3.14). One record may carry multiple character-strings. + public readonly struct TxtRecord + { + private readonly IReadOnlyList? _values; + + public IReadOnlyList Values => _values ?? Array.Empty(); + public TimeSpan Ttl { get; } + + internal TxtRecord(IReadOnlyList values, TimeSpan ttl) + { + _values = values; + Ttl = ttl; + } + } + + /// A CNAME record (RFC 1035 §3.3.1). + public readonly struct CNameRecord + { + public string CanonicalName { get; } + public TimeSpan Ttl { get; } + + internal CNameRecord(string canonicalName, TimeSpan ttl) + { + CanonicalName = canonicalName; + Ttl = ttl; + } + } + + /// A PTR record (RFC 1035 §3.3.12). + public readonly struct PtrRecord + { + public string Name { get; } + public TimeSpan Ttl { get; } + + internal PtrRecord(string name, TimeSpan ttl) + { + Name = name; + Ttl = ttl; + } + } + + /// An NS record (RFC 1035 §3.3.11). + public readonly struct NsRecord + { + public string Name { get; } + public TimeSpan Ttl { get; } + + internal NsRecord(string name, TimeSpan ttl) + { + Name = name; + Ttl = ttl; + } + } +} diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Unsupported.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Unsupported.cs new file mode 100644 index 00000000000000..0e3e4543ae763c --- /dev/null +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Unsupported.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +#pragma warning disable CA1822 // Members do not access instance data and can be marked as static — but we keep them as instance to match the partial signatures on platforms that do implement DNS. + +namespace System.Net +{ + public sealed partial class DnsResolver + { + private Task> ResolveAddressesCoreAsync(string name, AddressFamily addressFamily, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + + private Task> ResolveSrvCoreAsync(string name, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + + private Task> ResolveMxCoreAsync(string name, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + + private Task> ResolveTxtCoreAsync(string name, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + + private Task> ResolveCNameCoreAsync(string name, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + + private Task> ResolvePtrCoreAsync(string name, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + + private Task> ResolveNsCoreAsync(string name, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + } +} diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Windows.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Windows.cs new file mode 100644 index 00000000000000..f7b8727f39e370 --- /dev/null +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Windows.cs @@ -0,0 +1,327 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net +{ + public sealed partial class DnsResolver + { + // ---- Public Resolve*Core methods (called from cross-platform DnsResolver) ---- + + private async Task> ResolveAddressesCoreAsync(string name, AddressFamily addressFamily, CancellationToken cancellationToken) + { + if (addressFamily == AddressFamily.Unspecified) + { + // Issue A and AAAA in parallel; merge results. + Task> aTask = QueryAddressesAsync(name, Interop.Dnsapi.DNS_TYPE_A, cancellationToken); + Task> aaaaTask = QueryAddressesAsync(name, Interop.Dnsapi.DNS_TYPE_AAAA, cancellationToken); + DnsResult aRes = await aTask.ConfigureAwait(false); + DnsResult aaaaRes = await aaaaTask.ConfigureAwait(false); + return MergeAddressResults(aRes, aaaaRes); + } + + ushort qtype = addressFamily switch + { + AddressFamily.InterNetwork => Interop.Dnsapi.DNS_TYPE_A, + AddressFamily.InterNetworkV6 => Interop.Dnsapi.DNS_TYPE_AAAA, + _ => throw new ArgumentException(SR.net_invalid_ip_addr, nameof(addressFamily)), + }; + return await QueryAddressesAsync(name, qtype, cancellationToken).ConfigureAwait(false); + } + + private async Task> ResolveSrvCoreAsync(string name, CancellationToken cancellationToken) + { + DnsQueryRawResult raw = await DnsQueryExAsync(name, Interop.Dnsapi.DNS_TYPE_SRV, cancellationToken).ConfigureAwait(false); + try + { + if (raw.ResponseCode != DnsResponseCode.NoError) + { + return new DnsResult(raw.ResponseCode, null, raw.NegativeCacheTtl); + } + + // Gather additional-section A/AAAA records by name so we can attach them. + Dictionary>? glue = null; + ParseAdditionalAddresses(raw.RecordsHead, ref glue); + + List records = new(); + for (IntPtr cur = raw.RecordsHead; cur != IntPtr.Zero; ) + { + Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); + uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; + if (hdr.wType == Interop.Dnsapi.DNS_TYPE_SRV && section == Interop.Dnsapi.DNSREC_ANSWER) + { + IntPtr dataPtr = cur + Marshal.SizeOf(); + Interop.Dnsapi.DNS_SRV_DATA data = Marshal.PtrToStructure(dataPtr); + string target = PtrToString(data.pNameTarget) ?? string.Empty; + IReadOnlyList? attached = null; + if (glue != null && glue.TryGetValue(target, out List? list)) + { + attached = list; + } + records.Add(new SrvRecord(target, data.wPort, data.wPriority, data.wWeight, TimeSpan.FromSeconds(hdr.dwTtl), attached)); + } + cur = hdr.pNext; + } + + return new DnsResult(DnsResponseCode.NoError, records, TimeSpan.Zero); + } + finally + { + raw.Dispose(); + } + } + + private Task> ResolveMxCoreAsync(string name, CancellationToken cancellationToken) + => QuerySimpleAsync(name, Interop.Dnsapi.DNS_TYPE_MX, cancellationToken, static (hdr, dataPtr) => + { + Interop.Dnsapi.DNS_MX_DATA data = Marshal.PtrToStructure(dataPtr); + return new MxRecord(PtrToString(data.pNameExchange) ?? string.Empty, data.wPreference, TimeSpan.FromSeconds(hdr.dwTtl)); + }); + + private Task> ResolveCNameCoreAsync(string name, CancellationToken cancellationToken) + => QuerySimpleAsync(name, Interop.Dnsapi.DNS_TYPE_CNAME, cancellationToken, static (hdr, dataPtr) => + { + Interop.Dnsapi.DNS_CNAME_DATA data = Marshal.PtrToStructure(dataPtr); + return new CNameRecord(PtrToString(data.pNameHost) ?? string.Empty, TimeSpan.FromSeconds(hdr.dwTtl)); + }); + + private Task> ResolvePtrCoreAsync(string name, CancellationToken cancellationToken) + => QuerySimpleAsync(name, Interop.Dnsapi.DNS_TYPE_PTR, cancellationToken, static (hdr, dataPtr) => + { + Interop.Dnsapi.DNS_PTR_DATA data = Marshal.PtrToStructure(dataPtr); + return new PtrRecord(PtrToString(data.pNameHost) ?? string.Empty, TimeSpan.FromSeconds(hdr.dwTtl)); + }); + + private Task> ResolveNsCoreAsync(string name, CancellationToken cancellationToken) + => QuerySimpleAsync(name, Interop.Dnsapi.DNS_TYPE_NS, cancellationToken, static (hdr, dataPtr) => + { + Interop.Dnsapi.DNS_NS_DATA data = Marshal.PtrToStructure(dataPtr); + return new NsRecord(PtrToString(data.pNameHost) ?? string.Empty, TimeSpan.FromSeconds(hdr.dwTtl)); + }); + + private async Task> ResolveTxtCoreAsync(string name, CancellationToken cancellationToken) + { + DnsQueryRawResult raw = await DnsQueryExAsync(name, Interop.Dnsapi.DNS_TYPE_TEXT, cancellationToken).ConfigureAwait(false); + try + { + if (raw.ResponseCode != DnsResponseCode.NoError) + { + return new DnsResult(raw.ResponseCode, null, raw.NegativeCacheTtl); + } + + List records = new(); + for (IntPtr cur = raw.RecordsHead; cur != IntPtr.Zero; ) + { + Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); + uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; + if (hdr.wType == Interop.Dnsapi.DNS_TYPE_TEXT && section == Interop.Dnsapi.DNSREC_ANSWER) + { + IntPtr dataPtr = cur + Marshal.SizeOf(); + // DNS_TXT_DATA: uint dwStringCount; followed by array of PCWSTR. + uint count = (uint)Marshal.ReadInt32(dataPtr); + int ptrSize = IntPtr.Size; + IntPtr stringsPtr = dataPtr + sizeof(uint); + if (ptrSize > sizeof(uint)) + { + // Round up to pointer alignment. + long aligned = ((long)stringsPtr + (ptrSize - 1)) & ~(long)(ptrSize - 1); + stringsPtr = checked((nint)aligned); + } + string[] values = new string[count]; + for (int i = 0; i < count; i++) + { + IntPtr strPtr = Marshal.ReadIntPtr(stringsPtr, i * ptrSize); + values[i] = PtrToString(strPtr) ?? string.Empty; + } + records.Add(new TxtRecord(values, TimeSpan.FromSeconds(hdr.dwTtl))); + } + cur = hdr.pNext; + } + + return new DnsResult(DnsResponseCode.NoError, records, TimeSpan.Zero); + } + finally + { + raw.Dispose(); + } + } + + // ---- Helpers for address parsing ---- + + private async Task> QueryAddressesAsync(string name, ushort qtype, CancellationToken cancellationToken) + { + DnsQueryRawResult raw = await DnsQueryExAsync(name, qtype, cancellationToken).ConfigureAwait(false); + try + { + if (raw.ResponseCode != DnsResponseCode.NoError) + { + return new DnsResult(raw.ResponseCode, null, raw.NegativeCacheTtl); + } + + List records = new(); + for (IntPtr cur = raw.RecordsHead; cur != IntPtr.Zero; ) + { + Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); + uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; + if (hdr.wType == qtype && section == Interop.Dnsapi.DNSREC_ANSWER) + { + IntPtr dataPtr = cur + Marshal.SizeOf(); + if (TryParseAddress(hdr.wType, dataPtr, out IPAddress? address)) + { + records.Add(new AddressRecord(address!, TimeSpan.FromSeconds(hdr.dwTtl))); + } + } + cur = hdr.pNext; + } + + return new DnsResult(DnsResponseCode.NoError, records, TimeSpan.Zero); + } + finally + { + raw.Dispose(); + } + } + + private static DnsResult MergeAddressResults(DnsResult a, DnsResult b) + { + if (a.Records.Count > 0 || b.Records.Count > 0) + { + AddressRecord[] merged = new AddressRecord[a.Records.Count + b.Records.Count]; + int idx = 0; + for (int i = 0; i < a.Records.Count; i++) merged[idx++] = a.Records[i]; + for (int i = 0; i < b.Records.Count; i++) merged[idx++] = b.Records[i]; + return new DnsResult(DnsResponseCode.NoError, merged, TimeSpan.Zero); + } + + DnsResponseCode chosenRc = a.ResponseCode == DnsResponseCode.NxDomain || b.ResponseCode == DnsResponseCode.NxDomain + ? DnsResponseCode.NxDomain + : (a.ResponseCode != DnsResponseCode.NoError ? a.ResponseCode : b.ResponseCode); + TimeSpan negTtl = a.NegativeCacheTtl > TimeSpan.Zero ? a.NegativeCacheTtl : b.NegativeCacheTtl; + return new DnsResult(chosenRc, null, negTtl); + } + + private static bool TryParseAddress(ushort recordType, IntPtr dataPtr, out IPAddress? address) + { + if (recordType == Interop.Dnsapi.DNS_TYPE_A) + { + uint ip = (uint)Marshal.ReadInt32(dataPtr); + address = new IPAddress(ip); + return true; + } + if (recordType == Interop.Dnsapi.DNS_TYPE_AAAA) + { + byte[] bytes = new byte[16]; + Marshal.Copy(dataPtr, bytes, 0, 16); + address = new IPAddress(bytes); + return true; + } + address = null; + return false; + } + + private static void ParseAdditionalAddresses(IntPtr head, ref Dictionary>? glue) + { + for (IntPtr cur = head; cur != IntPtr.Zero; ) + { + Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); + uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; + bool isAddress = hdr.wType == Interop.Dnsapi.DNS_TYPE_A || hdr.wType == Interop.Dnsapi.DNS_TYPE_AAAA; + if (section == Interop.Dnsapi.DNSREC_ADDITIONAL && isAddress) + { + IntPtr dataPtr = cur + Marshal.SizeOf(); + if (TryParseAddress(hdr.wType, dataPtr, out IPAddress? address)) + { + string name = PtrToString(hdr.pName) ?? string.Empty; + glue ??= new Dictionary>(StringComparer.OrdinalIgnoreCase); + if (!glue.TryGetValue(name, out List? list)) + { + list = new List(); + glue[name] = list; + } + list.Add(new AddressRecord(address!, TimeSpan.FromSeconds(hdr.dwTtl))); + } + } + cur = hdr.pNext; + } + } + + // ---- Generic single-record-type parser ---- + + private async Task> QuerySimpleAsync(string name, ushort qtype, CancellationToken cancellationToken, + Func selector) + { + DnsQueryRawResult raw = await DnsQueryExAsync(name, qtype, cancellationToken).ConfigureAwait(false); + try + { + if (raw.ResponseCode != DnsResponseCode.NoError) + { + return new DnsResult(raw.ResponseCode, null, raw.NegativeCacheTtl); + } + List records = new(); + for (IntPtr cur = raw.RecordsHead; cur != IntPtr.Zero; ) + { + Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); + uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; + if (hdr.wType == qtype && section == Interop.Dnsapi.DNSREC_ANSWER) + { + IntPtr dataPtr = cur + Marshal.SizeOf(); + records.Add(selector(hdr, dataPtr)); + } + cur = hdr.pNext; + } + return new DnsResult(DnsResponseCode.NoError, records, TimeSpan.Zero); + } + finally + { + raw.Dispose(); + } + } + + // ---- Core DnsQueryEx async wrapper ---- + + private unsafe Task DnsQueryExAsync(string name, ushort queryType, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + DnsQueryAsyncState state = new DnsQueryAsyncState(_options.Servers, name, queryType, cancellationToken); + return state.StartAsync(); + } + + private static unsafe string? PtrToString(IntPtr p) => + p == IntPtr.Zero ? null : Marshal.PtrToStringUni(p); + + // ---- Raw query result returned by the low-level helper ---- + + private readonly struct DnsQueryRawResult : IDisposable + { + public DnsResponseCode ResponseCode { get; } + public IntPtr RecordsHead { get; } + public TimeSpan NegativeCacheTtl { get; } + + public DnsQueryRawResult(DnsResponseCode responseCode, IntPtr recordsHead, TimeSpan negativeCacheTtl) + { + ResponseCode = responseCode; + RecordsHead = recordsHead; + NegativeCacheTtl = negativeCacheTtl; + } + + public void Dispose() + { + if (RecordsHead != IntPtr.Zero) + { + Interop.Dnsapi.DnsFree(RecordsHead, Interop.Dnsapi.DnsFreeRecordList); + } + } + } + } +} diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.WindowsAsync.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.WindowsAsync.cs new file mode 100644 index 00000000000000..07eec69db4308a --- /dev/null +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.WindowsAsync.cs @@ -0,0 +1,419 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net +{ + public sealed partial class DnsResolver + { + // Win11 build 22000 introduced DNS_QUERY_REQUEST3 (with pCustomServers). + private static readonly bool s_supportsCustomServers = + OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000); + + // Cached callback so we don't allocate a new delegate per query. + private static readonly Interop.Dnsapi.DnsQueryCompletionRoutine s_completionCallback = QueryCompletionCallback; + private static readonly IntPtr s_completionCallbackPtr = + Marshal.GetFunctionPointerForDelegate(s_completionCallback); + + /// + /// Holds the unmanaged state for a single DnsQueryEx invocation, including + /// the request/result/cancel structures, the pinned query name, and the + /// completion TaskCompletionSource. + /// + private sealed unsafe class DnsQueryAsyncState + { + private readonly TaskCompletionSource _tcs = + new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly string _name; + private readonly ushort _queryType; + private readonly CancellationToken _cancellationToken; + private readonly IList _servers; + + private GCHandle _selfHandle; + private IntPtr _namePtr; + private IntPtr _requestPtr; + private IntPtr _resultPtr; + private IntPtr _cancelPtr; + private IntPtr _serverListPtr; // DNS_ADDR_ARRAY (v1) buffer + private IntPtr _customServersPtr; // DNS_CUSTOM_SERVER[] (v3) buffer + private CancellationTokenRegistration _ctReg; + private int _completed; // 0 = pending, 1 = completed (callback or sync) + + public DnsQueryAsyncState(IList servers, string name, ushort queryType, CancellationToken cancellationToken) + { + _servers = servers; + _name = name; + _queryType = queryType; + _cancellationToken = cancellationToken; + } + + public Task StartAsync() + { + bool needsV3 = false; + if (_servers is { Count: > 0 }) + { + foreach (IPEndPoint ep in _servers) + { + if (ep.Port != 0 && ep.Port != 53) + { + needsV3 = true; + break; + } + } + } + + if (needsV3 && !s_supportsCustomServers) + { + throw new PlatformNotSupportedException(SR.net_dns_custom_port_not_supported); + } + + bool useV3 = needsV3 && s_supportsCustomServers; + + try + { + _namePtr = Marshal.StringToHGlobalUni(_name); + _resultPtr = Marshal.AllocHGlobal(sizeof(Interop.Dnsapi.DNS_QUERY_RESULT)); + NativeMemory.Clear((void*)_resultPtr, (nuint)sizeof(Interop.Dnsapi.DNS_QUERY_RESULT)); + Interop.Dnsapi.DNS_QUERY_RESULT* result = (Interop.Dnsapi.DNS_QUERY_RESULT*)_resultPtr; + result->Version = useV3 ? Interop.Dnsapi.DNS_QUERY_REQUEST_VERSION3 : Interop.Dnsapi.DNS_QUERY_REQUEST_VERSION1; + + _cancelPtr = Marshal.AllocHGlobal(sizeof(Interop.Dnsapi.DNS_QUERY_CANCEL)); + NativeMemory.Clear((void*)_cancelPtr, (nuint)sizeof(Interop.Dnsapi.DNS_QUERY_CANCEL)); + + _selfHandle = GCHandle.Alloc(this); + + int status; + if (useV3) + { + _requestPtr = Marshal.AllocHGlobal(sizeof(Interop.Dnsapi.DNS_QUERY_REQUEST3)); + NativeMemory.Clear((void*)_requestPtr, (nuint)sizeof(Interop.Dnsapi.DNS_QUERY_REQUEST3)); + Interop.Dnsapi.DNS_QUERY_REQUEST3* req = (Interop.Dnsapi.DNS_QUERY_REQUEST3*)_requestPtr; + req->Version = Interop.Dnsapi.DNS_QUERY_REQUEST_VERSION3; + req->QueryName = _namePtr; + req->QueryType = _queryType; + req->QueryOptions = Interop.Dnsapi.DNS_QUERY_STANDARD; + req->InterfaceIndex = 0; + req->pQueryCompletionCallback = s_completionCallbackPtr; + req->pQueryContext = GCHandle.ToIntPtr(_selfHandle); + + BuildCustomServers(_servers, out _customServersPtr, out uint count); + req->cCustomServers = count; + req->pCustomServers = (Interop.Dnsapi.DNS_CUSTOM_SERVER*)_customServersPtr; + + status = Interop.Dnsapi.DnsQueryEx( + (Interop.Dnsapi.DNS_QUERY_REQUEST*)_requestPtr, + (Interop.Dnsapi.DNS_QUERY_RESULT*)_resultPtr, + (Interop.Dnsapi.DNS_QUERY_CANCEL*)_cancelPtr); + } + else + { + _requestPtr = Marshal.AllocHGlobal(sizeof(Interop.Dnsapi.DNS_QUERY_REQUEST)); + NativeMemory.Clear((void*)_requestPtr, (nuint)sizeof(Interop.Dnsapi.DNS_QUERY_REQUEST)); + Interop.Dnsapi.DNS_QUERY_REQUEST* req = (Interop.Dnsapi.DNS_QUERY_REQUEST*)_requestPtr; + req->Version = Interop.Dnsapi.DNS_QUERY_REQUEST_VERSION1; + req->QueryName = _namePtr; + req->QueryType = _queryType; + req->QueryOptions = Interop.Dnsapi.DNS_QUERY_STANDARD; + req->InterfaceIndex = 0; + req->pQueryCompletionCallback = s_completionCallbackPtr; + req->pQueryContext = GCHandle.ToIntPtr(_selfHandle); + + if (_servers is { Count: > 0 }) + { + BuildAddrArray(_servers, out _serverListPtr); + req->pDnsServerList = (Interop.Dnsapi.DNS_ADDR_ARRAY*)_serverListPtr; + } + + status = Interop.Dnsapi.DnsQueryEx( + (Interop.Dnsapi.DNS_QUERY_REQUEST*)_requestPtr, + (Interop.Dnsapi.DNS_QUERY_RESULT*)_resultPtr, + (Interop.Dnsapi.DNS_QUERY_CANCEL*)_cancelPtr); + } + + if (status == Interop.Dnsapi.DNS_REQUEST_PENDING) + { + // Async. Register cancellation; the callback will free resources and complete the TCS. + if (_cancellationToken.CanBeCanceled) + { + _ctReg = _cancellationToken.UnsafeRegister(static (s, _) => + { + DnsQueryAsyncState st = (DnsQueryAsyncState)s!; + st.CancelAndAbort(); + }, this); + } + } + else + { + // Synchronous completion. The callback was NOT invoked; we complete inline. + CompleteFromResult(status); + } + } + catch + { + FreeAll(); + throw; + } + + return _tcs.Task; + } + + private void CancelAndAbort() + { + if (_cancelPtr != IntPtr.Zero) + { + Interop.Dnsapi.DnsCancelQuery((Interop.Dnsapi.DNS_QUERY_CANCEL*)_cancelPtr); + } + } + + /// + /// Invoked from either the native callback or the sync completion path. + /// Parses the QueryStatus and pQueryRecords from the result struct, + /// completes the TCS, and frees state. + /// + internal void CompleteFromResult(int status) + { + if (Interlocked.Exchange(ref _completed, 1) != 0) + { + return; + } + + try + { + _ctReg.Dispose(); + + Interop.Dnsapi.DNS_QUERY_RESULT result = Marshal.PtrToStructure(_resultPtr); + IntPtr records = result.pQueryRecords; + + if (_cancellationToken.IsCancellationRequested) + { + if (records != IntPtr.Zero) + { + Interop.Dnsapi.DnsFree(records, Interop.Dnsapi.DnsFreeRecordList); + } + _tcs.TrySetCanceled(_cancellationToken); + return; + } + + DnsResponseCode rc = MapWindowsErrorToResponseCode(status); + + // 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); + } + + _tcs.TrySetResult(new DnsQueryRawResult(rc, records, negativeTtl)); + } + catch (Exception ex) + { + _tcs.TrySetException(ex); + } + finally + { + FreeAll(); + } + } + + private void FreeAll() + { + if (_namePtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(_namePtr); + _namePtr = IntPtr.Zero; + } + if (_requestPtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(_requestPtr); + _requestPtr = IntPtr.Zero; + } + if (_resultPtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(_resultPtr); + _resultPtr = IntPtr.Zero; + } + if (_cancelPtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(_cancelPtr); + _cancelPtr = IntPtr.Zero; + } + if (_serverListPtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(_serverListPtr); + _serverListPtr = IntPtr.Zero; + } + if (_customServersPtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(_customServersPtr); + _customServersPtr = IntPtr.Zero; + } + if (_selfHandle.IsAllocated) + { + _selfHandle.Free(); + } + } + } + + // Native callback. Marshaled to a function pointer once at startup. + // We use a managed delegate (no UnmanagedCallersOnly) because callers + // currently pass it via Marshal.GetFunctionPointerForDelegate. + private static void QueryCompletionCallback(IntPtr pQueryContext, IntPtr pQueryResults) + { + try + { + GCHandle handle = GCHandle.FromIntPtr(pQueryContext); + DnsQueryAsyncState? state = handle.Target as DnsQueryAsyncState; + if (state == null) + { + return; + } + + // pQueryResults points to the same DNS_QUERY_RESULT we passed in. + unsafe + { + Interop.Dnsapi.DNS_QUERY_RESULT* res = (Interop.Dnsapi.DNS_QUERY_RESULT*)pQueryResults; + state.CompleteFromResult(res->QueryStatus); + } + } + catch + { + // Swallow — never allow exceptions to propagate into native code. + } + } + + private static unsafe void BuildAddrArray(IList servers, out IntPtr arrayPtr) + { + int count = servers.Count; + int headerSize = sizeof(Interop.Dnsapi.DNS_ADDR_ARRAY); + int addrSize = sizeof(Interop.Dnsapi.DNS_ADDR); + int totalSize = headerSize + addrSize * count; + + arrayPtr = Marshal.AllocHGlobal(totalSize); + NativeMemory.Clear((void*)arrayPtr, (nuint)totalSize); + + Interop.Dnsapi.DNS_ADDR_ARRAY* arr = (Interop.Dnsapi.DNS_ADDR_ARRAY*)arrayPtr; + arr->MaxCount = (uint)count; + arr->AddrCount = (uint)count; + arr->Family = (ushort)(servers[0].AddressFamily == AddressFamily.InterNetwork ? Interop.Dnsapi.AF_INET : Interop.Dnsapi.AF_INET6); + + byte* addrBase = (byte*)arrayPtr + headerSize; + for (int i = 0; i < count; i++) + { + IPEndPoint ep = servers[i]; + byte* sa = addrBase + (i * addrSize); + WriteSockAddr(sa, ep); + } + } + + private static unsafe void BuildCustomServers(IList servers, out IntPtr arrayPtr, out uint count) + { + if (servers is null or { Count: 0 }) + { + arrayPtr = IntPtr.Zero; + count = 0; + return; + } + + int n = servers.Count; + int entrySize = sizeof(Interop.Dnsapi.DNS_CUSTOM_SERVER); + arrayPtr = Marshal.AllocHGlobal(entrySize * n); + NativeMemory.Clear((void*)arrayPtr, (nuint)(entrySize * n)); + + Interop.Dnsapi.DNS_CUSTOM_SERVER* arr = (Interop.Dnsapi.DNS_CUSTOM_SERVER*)arrayPtr; + for (int i = 0; i < n; i++) + { + IPEndPoint ep = servers[i]; + arr[i].dwServerType = Interop.Dnsapi.DNS_CUSTOM_SERVER_TYPE_UDP; + arr[i].ullFlags = 0; + arr[i].pwszTemplate = IntPtr.Zero; + WriteSockAddr((byte*)&arr[i].ServerAddr[0], ep); + } + count = (uint)n; + } + + // 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) + { + int port = ep.Port == 0 ? 53 : ep.Port; + if (ep.AddressFamily == AddressFamily.InterNetwork) + { + // sockaddr_in: ushort family, ushort port (net order), uint addr, 8 bytes zero + *(ushort*)(dest + 0) = Interop.Dnsapi.AF_INET; + dest[2] = (byte)(port >> 8); + dest[3] = (byte)(port & 0xff); + Span 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]; + // dest[8..15] left zero + } + else if (ep.AddressFamily == AddressFamily.InterNetworkV6) + { + // sockaddr_in6: ushort family, ushort port, uint flowinfo, 16-byte addr, uint scope_id + *(ushort*)(dest + 0) = Interop.Dnsapi.AF_INET6; + dest[2] = (byte)(port >> 8); + dest[3] = (byte)(port & 0xff); + // flowinfo (dest[4..7]) left zero + Span addrBytes = stackalloc byte[16]; + ep.Address.TryWriteBytes(addrBytes, out _); + for (int i = 0; i < 16; i++) + { + dest[8 + i] = addrBytes[i]; + } + // scope_id (dest[24..27]) + uint scopeId = (uint)ep.Address.ScopeId; + dest[24] = (byte)(scopeId & 0xff); + dest[25] = (byte)((scopeId >> 8) & 0xff); + dest[26] = (byte)((scopeId >> 16) & 0xff); + dest[27] = (byte)((scopeId >> 24) & 0xff); + } + else + { + throw new ArgumentException(SR.net_invalid_ip_addr); + } + } + + private static DnsResponseCode MapWindowsErrorToResponseCode(int status) => + status switch + { + Interop.Dnsapi.ERROR_SUCCESS => DnsResponseCode.NoError, + Interop.Dnsapi.DNS_INFO_NO_RECORDS => DnsResponseCode.NoError, // NODATA: name exists but no records of requested type + Interop.Dnsapi.DNS_ERROR_RCODE_NAME_ERROR => DnsResponseCode.NxDomain, + Interop.Dnsapi.DNS_ERROR_RCODE_FORMAT_ERROR => DnsResponseCode.FormatError, + Interop.Dnsapi.DNS_ERROR_RCODE_SERVER_FAILURE => DnsResponseCode.ServerFailure, + Interop.Dnsapi.DNS_ERROR_RCODE_NOT_IMPLEMENTED => DnsResponseCode.NotImplemented, + Interop.Dnsapi.DNS_ERROR_RCODE_REFUSED => DnsResponseCode.Refused, + _ => DnsResponseCode.ServerFailure, + }; + + private static TimeSpan ExtractNegativeCacheTtl(IntPtr head) + { + // Walk the record list looking for an SOA in the authority section. + for (IntPtr cur = head; cur != IntPtr.Zero; ) + { + Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); + uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; + if (hdr.wType == Interop.Dnsapi.DNS_TYPE_SOA && section == Interop.Dnsapi.DNSREC_AUTHORITY) + { + IntPtr dataPtr = cur + Marshal.SizeOf(); + Interop.Dnsapi.DNS_SOA_DATA soa = Marshal.PtrToStructure(dataPtr); + // RFC 2308 §5: negative cache TTL = min(SOA TTL, SOA MINIMUM) + uint negTtl = Math.Min(hdr.dwTtl, soa.dwDefaultTtl); + return TimeSpan.FromSeconds(negTtl); + } + cur = hdr.pNext; + } + return TimeSpan.Zero; + } + } +} diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs new file mode 100644 index 00000000000000..01e78c216e08cc --- /dev/null +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs @@ -0,0 +1,163 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net +{ + /// + /// Resolves DNS records, optionally using a caller-specified set of DNS servers. + /// + /// + /// When constructed without options, or with empty , + /// the resolver uses the system-configured DNS servers. + /// + public sealed partial class DnsResolver : IAsyncDisposable, IDisposable + { + private readonly DnsResolverOptions _options; + private bool _disposed; + + public DnsResolver() : this(new DnsResolverOptions()) { } + + public DnsResolver(DnsResolverOptions options) + { + ArgumentNullException.ThrowIfNull(options); + _options = options; + } + + public DnsResult ResolveAddresses(string name) + => ResolveAddressesAsync(name).GetAwaiter().GetResult(); + + public DnsResult ResolveAddresses(string name, AddressFamily addressFamily) + => ResolveAddressesAsync(name, addressFamily).GetAwaiter().GetResult(); + + public DnsResult ResolveSrv(string name) + => ResolveSrvAsync(name).GetAwaiter().GetResult(); + + public DnsResult ResolveMx(string name) + => ResolveMxAsync(name).GetAwaiter().GetResult(); + + public DnsResult ResolveTxt(string name) + => ResolveTxtAsync(name).GetAwaiter().GetResult(); + + public DnsResult ResolveCName(string name) + => ResolveCNameAsync(name).GetAwaiter().GetResult(); + + public DnsResult ResolvePtr(string name) + => ResolvePtrAsync(name).GetAwaiter().GetResult(); + + public DnsResult ResolveNs(string name) + => ResolveNsAsync(name).GetAwaiter().GetResult(); + + public Task> ResolveAddressesAsync(string name, CancellationToken cancellationToken = default) + => ResolveAddressesAsync(name, AddressFamily.Unspecified, cancellationToken); + + public Task> ResolveAddressesAsync(string name, AddressFamily addressFamily, CancellationToken cancellationToken = default) + { + ValidateName(name); + ObjectDisposedException.ThrowIf(_disposed, this); + return ResolveAddressesCoreAsync(name, addressFamily, cancellationToken); + } + + public Task> ResolveSrvAsync(string name, CancellationToken cancellationToken = default) + { + ValidateName(name); + ObjectDisposedException.ThrowIf(_disposed, this); + return ResolveSrvCoreAsync(name, cancellationToken); + } + + public Task> ResolveMxAsync(string name, CancellationToken cancellationToken = default) + { + ValidateName(name); + ObjectDisposedException.ThrowIf(_disposed, this); + return ResolveMxCoreAsync(name, cancellationToken); + } + + public Task> ResolveTxtAsync(string name, CancellationToken cancellationToken = default) + { + ValidateName(name); + ObjectDisposedException.ThrowIf(_disposed, this); + return ResolveTxtCoreAsync(name, cancellationToken); + } + + public Task> ResolveCNameAsync(string name, CancellationToken cancellationToken = default) + { + ValidateName(name); + ObjectDisposedException.ThrowIf(_disposed, this); + return ResolveCNameCoreAsync(name, cancellationToken); + } + + public Task> ResolvePtrAsync(string name, CancellationToken cancellationToken = default) + { + ValidateName(name); + ObjectDisposedException.ThrowIf(_disposed, this); + return ResolvePtrCoreAsync(name, cancellationToken); + } + + public Task> ResolvePtrAsync(IPAddress address, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(address); + ObjectDisposedException.ThrowIf(_disposed, this); + return ResolvePtrCoreAsync(BuildArpaName(address), cancellationToken); + } + + public Task> ResolveNsAsync(string name, CancellationToken cancellationToken = default) + { + ValidateName(name); + ObjectDisposedException.ThrowIf(_disposed, this); + return ResolveNsCoreAsync(name, cancellationToken); + } + + public void Dispose() => _disposed = true; + + public ValueTask DisposeAsync() + { + _disposed = true; + return ValueTask.CompletedTask; + } + + private static void ValidateName(string name) + { + ArgumentException.ThrowIfNullOrEmpty(name); + } + + /// + /// Builds the reverse-lookup .arpa domain name for an IPv4 or IPv6 address. + /// + internal static unsafe string BuildArpaName(IPAddress address) + { + if (address.AddressFamily == AddressFamily.InterNetwork) + { + Span 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"); + } + else if (address.AddressFamily == AddressFamily.InterNetworkV6) + { + Span bytes = stackalloc byte[16]; + address.TryWriteBytes(bytes, out _); + Span 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)); + + static char ToHex(int n) => (char)(n < 10 ? '0' + n : 'a' + (n - 10)); + } + else + { + throw new ArgumentException(SR.net_invalid_ip_addr, nameof(address)); + } + } + } +} diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverOptions.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverOptions.cs new file mode 100644 index 00000000000000..d9bb6c9dffbf6b --- /dev/null +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverOptions.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace System.Net +{ + /// + /// Options controlling DNS resolution performed by . + /// + public sealed class DnsResolverOptions + { + /// + /// DNS servers to query. When empty, the system-configured DNS servers are used. + /// + public IList Servers { get; set; } = new List(); + } +} diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResponseCode.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResponseCode.cs new file mode 100644 index 00000000000000..9b2b99f3d91e56 --- /dev/null +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResponseCode.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Net +{ + /// DNS response codes as defined in RFC 1035 (and updates). + [CLSCompliant(false)] + public enum DnsResponseCode : ushort + { + /// No error condition. + NoError = 0, + /// The name server was unable to interpret the query. + FormatError = 1, + /// The name server was unable to process this query due to a problem with the name server. + ServerFailure = 2, + /// The domain name referenced in the query does not exist (NXDOMAIN). + NxDomain = 3, + /// The name server does not support the requested kind of query. + NotImplemented = 4, + /// The name server refuses to perform the specified operation for policy reasons. + Refused = 5, + } +} diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResult.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResult.cs new file mode 100644 index 00000000000000..702445b4166d65 --- /dev/null +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResult.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace System.Net +{ + /// + /// Carries the result of a DNS resolution operation, including the response + /// code, the parsed records, and (for negative responses) the negative-cache TTL. + /// + public readonly struct DnsResult + { + private readonly IReadOnlyList? _records; + + /// The DNS response code returned by the server. + [CLSCompliant(false)] + public DnsResponseCode ResponseCode { get; } + + /// + /// The records returned by the server. Empty on error or NODATA responses. + /// + public IReadOnlyList Records => _records ?? Array.Empty(); + + /// + /// For negative responses (NXDOMAIN/NODATA), the TTL for which the negative + /// answer may be cached (derived from the SOA minimum TTL in the authority + /// section, per RFC 2308 §5). if not applicable + /// or unavailable. + /// + public TimeSpan NegativeCacheTtl { get; } + + internal DnsResult(DnsResponseCode responseCode, IReadOnlyList? records, TimeSpan negativeCacheTtl) + { + ResponseCode = responseCode; + _records = records; + NegativeCacheTtl = negativeCacheTtl; + } + } +} From 77dd68ceab76e7db05f59e32b440f0cbaa45ac06 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Wed, 3 Jun 2026 15:45:11 +0200 Subject: [PATCH 02/19] Add DnsResolver tests 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> --- .../tests/FunctionalTests/DnsResolverTest.cs | 228 ++++++++++++++++++ ...Net.NameResolution.Functional.Tests.csproj | 1 + 2 files changed, 229 insertions(+) create mode 100644 src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs new file mode 100644 index 00000000000000..8307518af9ee3e --- /dev/null +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs @@ -0,0 +1,228 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace System.Net.NameResolution.Tests +{ + // Tests for the new DnsResolver / Dns.Resolve* APIs. + // Network tests are individually marked with [OuterLoop]. + public class DnsResolverTest + { + private const string TestHost = "microsoft.com"; + private const string TestSrv = "_sip._tls.microsoft.com"; // SRV record for SIP discovery + private const string TestMxHost = "microsoft.com"; + private const string TestTxtHost = "microsoft.com"; + private const string TestCNameHost = "www.microsoft.com"; + private const string TestNsHost = "microsoft.com"; + private const string NonExistentHost = "this-name-definitely-does-not-exist.dotnet-test.invalid"; + + // ---- Cross-platform argument-validation tests ---- + + [Fact] + public void DnsResolver_Construct_NullOptions_Throws() + { + Assert.Throws(() => new DnsResolver(null!)); + } + + [Fact] + public void DnsResolver_Construct_DefaultOptions_DoesNotThrow() + { + using DnsResolver r = new DnsResolver(); + Assert.NotNull(r); + } + + [Fact] + public async Task DnsResolver_NullName_Throws() + { + using DnsResolver r = new DnsResolver(); + await Assert.ThrowsAsync(() => r.ResolveAddressesAsync(null!)); + await Assert.ThrowsAsync(() => r.ResolveSrvAsync(null!)); + await Assert.ThrowsAsync(() => r.ResolveMxAsync(null!)); + await Assert.ThrowsAsync(() => r.ResolveTxtAsync(null!)); + await Assert.ThrowsAsync(() => r.ResolveCNameAsync(null!)); + await Assert.ThrowsAsync(() => r.ResolvePtrAsync((string)null!)); + await Assert.ThrowsAsync(() => r.ResolvePtrAsync((IPAddress)null!)); + await Assert.ThrowsAsync(() => r.ResolveNsAsync(null!)); + } + + [Fact] + public async Task DnsResolver_EmptyName_Throws() + { + using DnsResolver r = new DnsResolver(); + await Assert.ThrowsAsync(() => r.ResolveAddressesAsync(string.Empty)); + } + + [Fact] + public async Task DnsResolver_Disposed_Throws() + { + DnsResolver r = new DnsResolver(); + r.Dispose(); + await Assert.ThrowsAsync(() => r.ResolveAddressesAsync(TestHost)); + await Assert.ThrowsAsync(() => r.ResolveSrvAsync(TestSrv)); + await Assert.ThrowsAsync(() => r.ResolveMxAsync(TestMxHost)); + } + + [Fact] + public async Task DnsResolver_DisposeAsync_ThrowsOnUse() + { + DnsResolver r = new DnsResolver(); + await r.DisposeAsync(); + await Assert.ThrowsAsync(() => r.ResolveAddressesAsync(TestHost)); + } + + [Fact] + public async Task DnsResolver_PreCanceledToken_ReturnsCanceled() + { + using DnsResolver r = new DnsResolver(); + CancellationTokenSource cts = new CancellationTokenSource(); + cts.Cancel(); + await Assert.ThrowsAsync(() => r.ResolveAddressesAsync(TestHost, cts.Token)); + } + + // ---- Windows network tests (require outbound DNS) ---- + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [OuterLoop] + public async Task ResolveAddresses_KnownName_ReturnsRecords() + { + using DnsResolver r = new DnsResolver(); + DnsResult result = await r.ResolveAddressesAsync(TestHost); + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + Assert.NotEmpty(result.Records); + foreach (AddressRecord rec in result.Records) + { + Assert.NotNull(rec.Address); + Assert.True(rec.Address.AddressFamily == AddressFamily.InterNetwork || rec.Address.AddressFamily == AddressFamily.InterNetworkV6); + } + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [OuterLoop] + public async Task ResolveAddresses_IPv4Only_ReturnsOnlyIPv4() + { + using DnsResolver r = new DnsResolver(); + DnsResult result = await r.ResolveAddressesAsync(TestHost, AddressFamily.InterNetwork); + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + foreach (AddressRecord rec in result.Records) + { + Assert.Equal(AddressFamily.InterNetwork, rec.Address.AddressFamily); + } + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [OuterLoop] + public async Task ResolveAddresses_NonExistent_ReturnsNxDomain() + { + using DnsResolver r = new DnsResolver(); + DnsResult result = await r.ResolveAddressesAsync(NonExistentHost); + Assert.Equal(DnsResponseCode.NxDomain, result.ResponseCode); + Assert.Empty(result.Records); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [OuterLoop] + public async Task ResolveMx_KnownName_ReturnsRecords() + { + using DnsResolver r = new DnsResolver(); + DnsResult result = await r.ResolveMxAsync(TestMxHost); + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + Assert.NotEmpty(result.Records); + foreach (MxRecord rec in result.Records) + { + Assert.False(string.IsNullOrEmpty(rec.Exchange)); + } + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [OuterLoop] + public async Task ResolveTxt_KnownName_ReturnsRecords() + { + using DnsResolver r = new DnsResolver(); + DnsResult result = await r.ResolveTxtAsync(TestTxtHost); + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + Assert.NotEmpty(result.Records); + foreach (TxtRecord rec in result.Records) + { + Assert.NotEmpty(rec.Values); + } + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [OuterLoop] + public async Task ResolveCName_KnownName_ReturnsRecord() + { + using DnsResolver r = new DnsResolver(); + DnsResult result = await r.ResolveCNameAsync(TestCNameHost); + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + // CNAME may or may not exist for the target; at minimum the call should succeed. + if (result.Records.Count > 0) + { + Assert.False(string.IsNullOrEmpty(result.Records[0].CanonicalName)); + } + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [OuterLoop] + public async Task ResolveNs_KnownName_ReturnsRecords() + { + using DnsResolver r = new DnsResolver(); + DnsResult result = await r.ResolveNsAsync(TestNsHost); + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + Assert.NotEmpty(result.Records); + foreach (NsRecord rec in result.Records) + { + Assert.False(string.IsNullOrEmpty(rec.Name)); + } + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [OuterLoop] + public async Task ResolvePtr_ByIPAddress_ReturnsRecord() + { + using DnsResolver r = new DnsResolver(); + DnsResult result = await r.ResolvePtrAsync(IPAddress.Parse("8.8.8.8")); + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + Assert.NotEmpty(result.Records); + Assert.False(string.IsNullOrEmpty(result.Records[0].Name)); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [OuterLoop] + public async Task Static_Dns_ResolveAddressesAsync_Works() + { + DnsResult result = await Dns.ResolveAddressesAsync(TestHost); + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + Assert.NotEmpty(result.Records); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [OuterLoop] + public async Task DnsResolver_CustomServer_Port53_Works() + { + DnsResolverOptions opts = new DnsResolverOptions + { + Servers = { new IPEndPoint(IPAddress.Parse("8.8.8.8"), 53) } + }; + using DnsResolver r = new DnsResolver(opts); + DnsResult result = await r.ResolveAddressesAsync(TestHost); + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + Assert.NotEmpty(result.Records); + } + + // ---- Reverse-arpa name building (covers both IPv4 and IPv6 paths used by ResolvePtr(IPAddress)) ---- + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [OuterLoop] + public async Task ResolvePtr_IPv6Address_DoesNotThrow() + { + using DnsResolver r = new DnsResolver(); + // Google public DNS IPv6 — call shouldn't throw, even if no PTR record exists. + DnsResult result = await r.ResolvePtrAsync(IPAddress.Parse("2001:4860:4860::8888")); + Assert.True(result.ResponseCode == DnsResponseCode.NoError || result.ResponseCode == DnsResponseCode.NxDomain); + } + } +} diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/System.Net.NameResolution.Functional.Tests.csproj b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/System.Net.NameResolution.Functional.Tests.csproj index 9741c993ccdba5..bc260f9daf3b20 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/System.Net.NameResolution.Functional.Tests.csproj +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/System.Net.NameResolution.Functional.Tests.csproj @@ -9,6 +9,7 @@ + From f0ef4af53d4a1361570b9d102da29484ee2899df Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Thu, 4 Jun 2026 18:24:17 +0200 Subject: [PATCH 03/19] Add loopback DNS test suite and restrict DnsQueryEx custom servers to 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> --- .../Windows/Dnsapi/Interop.DnsTypes.cs | 2 + .../System/Net/DnsResolver.WindowsAsync.cs | 129 ++--- .../DnsResolverLoopbackTest.cs | 443 ++++++++++++++++++ .../tests/FunctionalTests/DnsResolverTest.cs | 12 + .../FunctionalTests/DnsResponseBuilder.cs | 243 ++++++++++ .../FunctionalTests/LoopbackDnsServer.cs | 275 +++++++++++ ...Net.NameResolution.Functional.Tests.csproj | 3 + 7 files changed, 1010 insertions(+), 97 deletions(-) create mode 100644 src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs create mode 100644 src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResponseBuilder.cs create mode 100644 src/libraries/System.Net.NameResolution/tests/FunctionalTests/LoopbackDnsServer.cs diff --git a/src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsTypes.cs b/src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsTypes.cs index cf92e3e0307a29..db3519b1c5ef8b 100644 --- a/src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsTypes.cs +++ b/src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsTypes.cs @@ -34,6 +34,8 @@ internal unsafe struct DNS_QUERY_REQUEST3 public uint InterfaceIndex; public IntPtr pQueryCompletionCallback; public IntPtr pQueryContext; + public int IsNetworkQueryRequired; // BOOL + public uint RequiredNetworkIndex; public uint cCustomServers; public DNS_CUSTOM_SERVER* pCustomServers; } diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.WindowsAsync.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.WindowsAsync.cs index 07eec69db4308a..86e68a2f26c6ad 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.WindowsAsync.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.WindowsAsync.cs @@ -12,10 +12,6 @@ namespace System.Net { public sealed partial class DnsResolver { - // Win11 build 22000 introduced DNS_QUERY_REQUEST3 (with pCustomServers). - private static readonly bool s_supportsCustomServers = - OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000); - // Cached callback so we don't allocate a new delegate per query. private static readonly Interop.Dnsapi.DnsQueryCompletionRoutine s_completionCallback = QueryCompletionCallback; private static readonly IntPtr s_completionCallbackPtr = @@ -40,8 +36,7 @@ private sealed unsafe class DnsQueryAsyncState private IntPtr _requestPtr; private IntPtr _resultPtr; private IntPtr _cancelPtr; - private IntPtr _serverListPtr; // DNS_ADDR_ARRAY (v1) buffer - private IntPtr _customServersPtr; // DNS_CUSTOM_SERVER[] (v3) buffer + private IntPtr _serverListPtr; // DNS_ADDR_ARRAY buffer private CancellationTokenRegistration _ctReg; private int _completed; // 0 = pending, 1 = completed (callback or sync) @@ -55,86 +50,56 @@ public DnsQueryAsyncState(IList servers, string name, ushort queryTy public Task StartAsync() { - bool needsV3 = false; + // DnsQueryEx only supports DNS servers reachable on the standard port 53. + // The sockaddr port field passed to the API must be 0 (the API always + // queries port 53); supplying any non-zero port - even 53 itself - results + // in ERROR_INVALID_PARAMETER. We therefore reject any server endpoint that + // requests a non-default port, since it cannot be honored on Windows. if (_servers is { Count: > 0 }) { foreach (IPEndPoint ep in _servers) { if (ep.Port != 0 && ep.Port != 53) { - needsV3 = true; - break; + throw new PlatformNotSupportedException(SR.net_dns_custom_port_not_supported); } } } - if (needsV3 && !s_supportsCustomServers) - { - throw new PlatformNotSupportedException(SR.net_dns_custom_port_not_supported); - } - - bool useV3 = needsV3 && s_supportsCustomServers; - try { _namePtr = Marshal.StringToHGlobalUni(_name); _resultPtr = Marshal.AllocHGlobal(sizeof(Interop.Dnsapi.DNS_QUERY_RESULT)); NativeMemory.Clear((void*)_resultPtr, (nuint)sizeof(Interop.Dnsapi.DNS_QUERY_RESULT)); Interop.Dnsapi.DNS_QUERY_RESULT* result = (Interop.Dnsapi.DNS_QUERY_RESULT*)_resultPtr; - result->Version = useV3 ? Interop.Dnsapi.DNS_QUERY_REQUEST_VERSION3 : Interop.Dnsapi.DNS_QUERY_REQUEST_VERSION1; + result->Version = Interop.Dnsapi.DNS_QUERY_REQUEST_VERSION1; _cancelPtr = Marshal.AllocHGlobal(sizeof(Interop.Dnsapi.DNS_QUERY_CANCEL)); NativeMemory.Clear((void*)_cancelPtr, (nuint)sizeof(Interop.Dnsapi.DNS_QUERY_CANCEL)); _selfHandle = GCHandle.Alloc(this); - int status; - if (useV3) + _requestPtr = Marshal.AllocHGlobal(sizeof(Interop.Dnsapi.DNS_QUERY_REQUEST)); + NativeMemory.Clear((void*)_requestPtr, (nuint)sizeof(Interop.Dnsapi.DNS_QUERY_REQUEST)); + Interop.Dnsapi.DNS_QUERY_REQUEST* req = (Interop.Dnsapi.DNS_QUERY_REQUEST*)_requestPtr; + req->Version = Interop.Dnsapi.DNS_QUERY_REQUEST_VERSION1; + req->QueryName = _namePtr; + req->QueryType = _queryType; + req->QueryOptions = Interop.Dnsapi.DNS_QUERY_STANDARD; + req->InterfaceIndex = 0; + req->pQueryCompletionCallback = s_completionCallbackPtr; + req->pQueryContext = GCHandle.ToIntPtr(_selfHandle); + + if (_servers is { Count: > 0 }) { - _requestPtr = Marshal.AllocHGlobal(sizeof(Interop.Dnsapi.DNS_QUERY_REQUEST3)); - NativeMemory.Clear((void*)_requestPtr, (nuint)sizeof(Interop.Dnsapi.DNS_QUERY_REQUEST3)); - Interop.Dnsapi.DNS_QUERY_REQUEST3* req = (Interop.Dnsapi.DNS_QUERY_REQUEST3*)_requestPtr; - req->Version = Interop.Dnsapi.DNS_QUERY_REQUEST_VERSION3; - req->QueryName = _namePtr; - req->QueryType = _queryType; - req->QueryOptions = Interop.Dnsapi.DNS_QUERY_STANDARD; - req->InterfaceIndex = 0; - req->pQueryCompletionCallback = s_completionCallbackPtr; - req->pQueryContext = GCHandle.ToIntPtr(_selfHandle); - - BuildCustomServers(_servers, out _customServersPtr, out uint count); - req->cCustomServers = count; - req->pCustomServers = (Interop.Dnsapi.DNS_CUSTOM_SERVER*)_customServersPtr; - - status = Interop.Dnsapi.DnsQueryEx( - (Interop.Dnsapi.DNS_QUERY_REQUEST*)_requestPtr, - (Interop.Dnsapi.DNS_QUERY_RESULT*)_resultPtr, - (Interop.Dnsapi.DNS_QUERY_CANCEL*)_cancelPtr); + BuildAddrArray(_servers, out _serverListPtr); + req->pDnsServerList = (Interop.Dnsapi.DNS_ADDR_ARRAY*)_serverListPtr; } - else - { - _requestPtr = Marshal.AllocHGlobal(sizeof(Interop.Dnsapi.DNS_QUERY_REQUEST)); - NativeMemory.Clear((void*)_requestPtr, (nuint)sizeof(Interop.Dnsapi.DNS_QUERY_REQUEST)); - Interop.Dnsapi.DNS_QUERY_REQUEST* req = (Interop.Dnsapi.DNS_QUERY_REQUEST*)_requestPtr; - req->Version = Interop.Dnsapi.DNS_QUERY_REQUEST_VERSION1; - req->QueryName = _namePtr; - req->QueryType = _queryType; - req->QueryOptions = Interop.Dnsapi.DNS_QUERY_STANDARD; - req->InterfaceIndex = 0; - req->pQueryCompletionCallback = s_completionCallbackPtr; - req->pQueryContext = GCHandle.ToIntPtr(_selfHandle); - - if (_servers is { Count: > 0 }) - { - BuildAddrArray(_servers, out _serverListPtr); - req->pDnsServerList = (Interop.Dnsapi.DNS_ADDR_ARRAY*)_serverListPtr; - } - status = Interop.Dnsapi.DnsQueryEx( - (Interop.Dnsapi.DNS_QUERY_REQUEST*)_requestPtr, - (Interop.Dnsapi.DNS_QUERY_RESULT*)_resultPtr, - (Interop.Dnsapi.DNS_QUERY_CANCEL*)_cancelPtr); - } + int status = Interop.Dnsapi.DnsQueryEx( + (Interop.Dnsapi.DNS_QUERY_REQUEST*)_requestPtr, + (Interop.Dnsapi.DNS_QUERY_RESULT*)_resultPtr, + (Interop.Dnsapi.DNS_QUERY_CANCEL*)_cancelPtr); if (status == Interop.Dnsapi.DNS_REQUEST_PENDING) { @@ -249,11 +214,6 @@ private void FreeAll() Marshal.FreeHGlobal(_serverListPtr); _serverListPtr = IntPtr.Zero; } - if (_customServersPtr != IntPtr.Zero) - { - Marshal.FreeHGlobal(_customServersPtr); - _customServersPtr = IntPtr.Zero; - } if (_selfHandle.IsAllocated) { _selfHandle.Free(); @@ -312,43 +272,19 @@ private static unsafe void BuildAddrArray(IList servers, out IntPtr } } - private static unsafe void BuildCustomServers(IList servers, out IntPtr arrayPtr, out uint count) - { - if (servers is null or { Count: 0 }) - { - arrayPtr = IntPtr.Zero; - count = 0; - return; - } - - int n = servers.Count; - int entrySize = sizeof(Interop.Dnsapi.DNS_CUSTOM_SERVER); - arrayPtr = Marshal.AllocHGlobal(entrySize * n); - NativeMemory.Clear((void*)arrayPtr, (nuint)(entrySize * n)); - - Interop.Dnsapi.DNS_CUSTOM_SERVER* arr = (Interop.Dnsapi.DNS_CUSTOM_SERVER*)arrayPtr; - for (int i = 0; i < n; i++) - { - IPEndPoint ep = servers[i]; - arr[i].dwServerType = Interop.Dnsapi.DNS_CUSTOM_SERVER_TYPE_UDP; - arr[i].ullFlags = 0; - arr[i].pwszTemplate = IntPtr.Zero; - WriteSockAddr((byte*)&arr[i].ServerAddr[0], ep); - } - count = (uint)n; - } - // 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) { - int port = ep.Port == 0 ? 53 : ep.Port; + // DnsQueryEx always queries DNS servers on port 53 and requires the sockaddr + // port field to be left as 0. Supplying a non-zero port (even 53) is rejected + // with ERROR_INVALID_PARAMETER. Non-default ports are validated and rejected + // earlier in StartAsync, so the port is always written as 0 here. if (ep.AddressFamily == AddressFamily.InterNetwork) { // sockaddr_in: ushort family, ushort port (net order), uint addr, 8 bytes zero *(ushort*)(dest + 0) = Interop.Dnsapi.AF_INET; - dest[2] = (byte)(port >> 8); - dest[3] = (byte)(port & 0xff); + // dest[2..3] (port) left zero Span addrBytes = stackalloc byte[4]; ep.Address.TryWriteBytes(addrBytes, out _); dest[4] = addrBytes[0]; @@ -361,8 +297,7 @@ private static unsafe void WriteSockAddr(byte* dest, IPEndPoint ep) { // sockaddr_in6: ushort family, ushort port, uint flowinfo, 16-byte addr, uint scope_id *(ushort*)(dest + 0) = Interop.Dnsapi.AF_INET6; - dest[2] = (byte)(port >> 8); - dest[3] = (byte)(port & 0xff); + // dest[2..3] (port) left zero // flowinfo (dest[4..7]) left zero Span addrBytes = stackalloc byte[16]; ep.Address.TryWriteBytes(addrBytes, out _); diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs new file mode 100644 index 00000000000000..e713e60bdda6dc --- /dev/null +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs @@ -0,0 +1,443 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace System.Net.NameResolution.Tests +{ + // Deterministic DnsResolver tests driven by an in-process loopback DNS server. + // + // On Windows, DnsQueryEx only ever contacts custom DNS servers on the standard + // port 53 (the sockaddr port field must be 0), so the loopback server binds port 53. + // When that port is unavailable (e.g. a local DNS service is already running) the + // tests are skipped via SkipTestException rather than failing. Because the single + // machine-wide port 53 is shared, these tests run sequentially (see the collection). + // + // These tests cover the record-parsing and response-handling behavior that the + // OuterLoop tests in DnsResolverTest.cs cannot exercise deterministically. + [Collection(nameof(DnsLoopbackTestCollection))] + public class DnsResolverLoopbackTest + { + public static bool IsSupported => PlatformDetection.IsWindows; + + private static DnsResolver CreateResolver(LoopbackDnsServer server) + => new DnsResolver(new DnsResolverOptions { Servers = { server.EndPoint } }); + + // Generates a unique multi-label name so neither the OS resolver cache nor a + // previous test run can satisfy the query without reaching the loopback server. + private static string UniqueName(string label) => $"{label}-{Guid.NewGuid():N}.test"; + + // ---- Address resolution ---- + + [ConditionalFact(nameof(IsSupported))] + [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + public async Task ResolveAddresses_Unspecified_ReturnsBothV4AndV6() + { + await using LoopbackDnsServer server = LoopbackDnsServer.Start(); + using DnsResolver resolver = CreateResolver(server); + + string name = UniqueName("host"); + server.AddResponse(name, DnsRecordType.A, b => b.Answer(new byte[] { 10, 0, 0, 1 }, ttl: 120)); + server.AddResponse(name, DnsRecordType.AAAA, b => b.Answer(IPAddress.Parse("fd00::1").GetAddressBytes(), ttl: 60)); + + DnsResult result = await resolver.ResolveAddressesAsync(name); + + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + Assert.Equal(2, result.Records.Count); + Assert.Contains(result.Records, a => a.Address.ToString() == "10.0.0.1"); + Assert.Contains(result.Records, a => a.Address.ToString() == "fd00::1"); + } + + [ConditionalFact(nameof(IsSupported))] + [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + public async Task ResolveAddresses_IPv4Only_ReturnsOnlyV4() + { + await using LoopbackDnsServer server = LoopbackDnsServer.Start(); + using DnsResolver resolver = CreateResolver(server); + + string name = UniqueName("v4"); + server.AddResponse(name, DnsRecordType.A, b => b.Answer(new byte[] { 10, 0, 0, 2 }, ttl: 300)); + server.AddResponse(name, DnsRecordType.AAAA, b => b.ResponseCode(DnsResponseCode.NxDomain)); + + DnsResult result = await resolver.ResolveAddressesAsync(name); + + // A succeeds, AAAA returns NXDOMAIN — overall is success because we got addresses. + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + AddressRecord record = Assert.Single(result.Records); + Assert.Equal("10.0.0.2", record.Address.ToString()); + } + + [ConditionalFact(nameof(IsSupported))] + [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + public async Task ResolveAddresses_IPv6Only_ReturnsOnlyV6() + { + await using LoopbackDnsServer server = LoopbackDnsServer.Start(); + using DnsResolver resolver = CreateResolver(server); + + string name = UniqueName("v6"); + server.AddResponse(name, DnsRecordType.A, b => b.ResponseCode(DnsResponseCode.NxDomain)); + server.AddResponse(name, DnsRecordType.AAAA, b => b.Answer(IPAddress.Parse("fd00::1").GetAddressBytes(), ttl: 60)); + + DnsResult result = await resolver.ResolveAddressesAsync(name); + + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + AddressRecord record = Assert.Single(result.Records); + Assert.Equal("fd00::1", record.Address.ToString()); + } + + [ConditionalFact(nameof(IsSupported))] + [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + public async Task ResolveAddresses_AddressFamilyV4_QueriesOnlyA() + { + await using LoopbackDnsServer server = LoopbackDnsServer.Start(); + using DnsResolver resolver = CreateResolver(server); + + string name = UniqueName("famv4"); + server.AddResponse(name, DnsRecordType.A, b => b.Answer(new byte[] { 192, 0, 2, 7 }, ttl: 200)); + + DnsResult result = await resolver.ResolveAddressesAsync(name, AddressFamily.InterNetwork); + + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + AddressRecord record = Assert.Single(result.Records); + Assert.Equal("192.0.2.7", record.Address.ToString()); + } + + [ConditionalFact(nameof(IsSupported))] + [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + public async Task ResolveAddresses_HasTtl() + { + await using LoopbackDnsServer server = LoopbackDnsServer.Start(); + using DnsResolver resolver = CreateResolver(server); + + string name = UniqueName("ttl"); + server.AddResponse(name, DnsRecordType.A, b => b.Answer(new byte[] { 10, 0, 0, 1 }, ttl: 120)); + server.AddResponse(name, DnsRecordType.AAAA, b => b.ResponseCode(DnsResponseCode.NxDomain)); + + DnsResult result = await resolver.ResolveAddressesAsync(name); + + AddressRecord record = Assert.Single(result.Records); + // The TTL we sent (120s) should be preserved (custom-server queries bypass the OS cache). + Assert.True(record.Ttl > TimeSpan.Zero && record.Ttl <= TimeSpan.FromSeconds(120), + $"Unexpected TTL: {record.Ttl}"); + } + + [ConditionalFact(nameof(IsSupported))] + [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + public async Task ResolveAddresses_Nxdomain_ReturnsNxDomain() + { + await using LoopbackDnsServer server = LoopbackDnsServer.Start(); + using DnsResolver resolver = CreateResolver(server); + + string name = UniqueName("missing"); + byte[] soaRdata = DnsResponseBuilder.BuildSoaRdata("test", 120); + server.AddResponse(name, DnsRecordType.A, b => b + .ResponseCode(DnsResponseCode.NxDomain) + .Authority("test", DnsRecordType.SOA, soaRdata, ttl: 120)); + server.AddResponse(name, DnsRecordType.AAAA, b => b + .ResponseCode(DnsResponseCode.NxDomain) + .Authority("test", DnsRecordType.SOA, soaRdata, ttl: 120)); + + DnsResult result = await resolver.ResolveAddressesAsync(name); + + Assert.Equal(DnsResponseCode.NxDomain, result.ResponseCode); + Assert.Empty(result.Records); + } + + [ConditionalFact(nameof(IsSupported))] + [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + public async Task ResolveAddresses_NoData_ReturnsNoErrorWithEmptyRecords() + { + await using LoopbackDnsServer server = LoopbackDnsServer.Start(); + using DnsResolver resolver = CreateResolver(server); + + string name = UniqueName("nodata"); + byte[] soaRdata = DnsResponseBuilder.BuildSoaRdata("test", 30); + server.AddResponse(name, DnsRecordType.A, b => b + .Authority("test", DnsRecordType.SOA, soaRdata, ttl: 30)); + server.AddResponse(name, DnsRecordType.AAAA, b => b + .Authority("test", DnsRecordType.SOA, soaRdata, ttl: 30)); + + // The name exists but has no A/AAAA records → NODATA for both queries. + DnsResult result = await resolver.ResolveAddressesAsync(name); + + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + Assert.Empty(result.Records); + } + + [ConditionalFact(nameof(IsSupported))] + [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + public async Task ResolveAddresses_NoData_And_Nxdomain_AreDistinguishable() + { + await using LoopbackDnsServer server = LoopbackDnsServer.Start(); + using DnsResolver resolver = CreateResolver(server); + + string nodataName = UniqueName("nodata"); + byte[] soaRdata = DnsResponseBuilder.BuildSoaRdata("test", 30); + server.AddResponse(nodataName, DnsRecordType.A, b => b + .Authority("test", DnsRecordType.SOA, soaRdata, ttl: 30)); + server.AddResponse(nodataName, DnsRecordType.AAAA, b => b + .Authority("test", DnsRecordType.SOA, soaRdata, ttl: 30)); + + string missingName = UniqueName("missing"); + byte[] nxSoaRdata = DnsResponseBuilder.BuildSoaRdata("test", 120); + server.AddResponse(missingName, DnsRecordType.A, b => b + .ResponseCode(DnsResponseCode.NxDomain) + .Authority("test", DnsRecordType.SOA, nxSoaRdata, ttl: 120)); + server.AddResponse(missingName, DnsRecordType.AAAA, b => b + .ResponseCode(DnsResponseCode.NxDomain) + .Authority("test", DnsRecordType.SOA, nxSoaRdata, ttl: 120)); + + DnsResult nodata = await resolver.ResolveAddressesAsync(nodataName); + Assert.Equal(DnsResponseCode.NoError, nodata.ResponseCode); + Assert.Empty(nodata.Records); + + DnsResult nxdomain = await resolver.ResolveAddressesAsync(missingName); + Assert.Equal(DnsResponseCode.NxDomain, nxdomain.ResponseCode); + Assert.Empty(nxdomain.Records); + + Assert.NotEqual(nodata.ResponseCode, nxdomain.ResponseCode); + } + + // ---- SRV ---- + + [ConditionalFact(nameof(IsSupported))] + [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + public async Task ResolveSrv_ReturnsRecords() + { + await using LoopbackDnsServer server = LoopbackDnsServer.Start(); + using DnsResolver resolver = CreateResolver(server); + + string name = $"_http._tcp.{UniqueName("svc")}"; + server.AddResponse(name, DnsRecordType.SRV, b => b + .Answer(DnsResponseBuilder.BuildSrvRdata(10, 100, 8080, "node1.test"), ttl: 120) + .Answer(DnsResponseBuilder.BuildSrvRdata(20, 50, 8081, "node2.test"), ttl: 120)); + + DnsResult result = await resolver.ResolveSrvAsync(name); + + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + Assert.Equal(2, result.Records.Count); + + SrvRecord s1 = Assert.Single(result.Records, s => s.Target == "node1.test"); + Assert.Equal((ushort)8080, s1.Port); + Assert.Equal((ushort)10, s1.Priority); + Assert.Equal((ushort)100, s1.Weight); + + SrvRecord s2 = Assert.Single(result.Records, s => s.Target == "node2.test"); + Assert.Equal((ushort)8081, s2.Port); + Assert.Equal((ushort)20, s2.Priority); + } + + [ConditionalFact(nameof(IsSupported))] + [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + public async Task ResolveSrv_IncludesAdditionalAddresses() + { + await using LoopbackDnsServer server = LoopbackDnsServer.Start(); + using DnsResolver resolver = CreateResolver(server); + + string name = $"_http._tcp.{UniqueName("svc")}"; + server.AddResponse(name, DnsRecordType.SRV, b => b + .Answer(DnsResponseBuilder.BuildSrvRdata(10, 100, 8080, "node1.test"), ttl: 120) + .Answer(DnsResponseBuilder.BuildSrvRdata(20, 50, 8081, "node2.test"), ttl: 120) + .Additional("node1.test", DnsRecordType.A, new byte[] { 10, 0, 0, 10 }, ttl: 120) + .Additional("node2.test", DnsRecordType.A, new byte[] { 10, 0, 0, 11 }, ttl: 120) + .Additional("node2.test", DnsRecordType.AAAA, IPAddress.Parse("fd00::11").GetAddressBytes(), ttl: 120)); + + DnsResult result = await resolver.ResolveSrvAsync(name); + + SrvRecord s1 = Assert.Single(result.Records, s => s.Target == "node1.test"); + Assert.NotNull(s1.Addresses); + AddressRecord s1Addr = Assert.Single(s1.Addresses); + Assert.Equal("10.0.0.10", s1Addr.Address.ToString()); + + SrvRecord s2 = Assert.Single(result.Records, s => s.Target == "node2.test"); + Assert.NotNull(s2.Addresses); + Assert.Equal(2, s2.Addresses.Count); + } + + [ConditionalFact(nameof(IsSupported))] + [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + public async Task ResolveSrv_NoAdditionalAddresses() + { + await using LoopbackDnsServer server = LoopbackDnsServer.Start(); + using DnsResolver resolver = CreateResolver(server); + + string name = $"_noadd._tcp.{UniqueName("svc")}"; + server.AddResponse(name, DnsRecordType.SRV, b => b + .Answer(DnsResponseBuilder.BuildSrvRdata(10, 100, 9090, "noaddr.test"), ttl: 60)); + + DnsResult result = await resolver.ResolveSrvAsync(name); + + SrvRecord record = Assert.Single(result.Records); + Assert.Equal("noaddr.test", record.Target); + Assert.Empty(record.Addresses); + } + + // ---- MX / TXT / CNAME / PTR / NS ---- + + [ConditionalFact(nameof(IsSupported))] + [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + public async Task ResolveMx_ReturnsRecords() + { + await using LoopbackDnsServer server = LoopbackDnsServer.Start(); + using DnsResolver resolver = CreateResolver(server); + + string name = UniqueName("mx"); + server.AddResponse(name, DnsRecordType.MX, b => b + .Answer(DnsResponseBuilder.BuildMxRdata(10, "mail1.test"), ttl: 120) + .Answer(DnsResponseBuilder.BuildMxRdata(20, "mail2.test"), ttl: 120)); + + DnsResult result = await resolver.ResolveMxAsync(name); + + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + Assert.Equal(2, result.Records.Count); + + MxRecord m1 = Assert.Single(result.Records, m => m.Exchange == "mail1.test"); + Assert.Equal((ushort)10, m1.Preference); + Assert.Single(result.Records, m => m.Exchange == "mail2.test" && m.Preference == 20); + } + + [ConditionalFact(nameof(IsSupported))] + [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + public async Task ResolveTxt_ReturnsValues() + { + await using LoopbackDnsServer server = LoopbackDnsServer.Start(); + using DnsResolver resolver = CreateResolver(server); + + string name = UniqueName("txt"); + server.AddResponse(name, DnsRecordType.TXT, b => b + .Answer(DnsResponseBuilder.BuildTxtRdata("v=spf1 -all"), ttl: 120) + .Answer(DnsResponseBuilder.BuildTxtRdata("part1", "part2"), ttl: 120)); + + DnsResult result = await resolver.ResolveTxtAsync(name); + + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + Assert.Equal(2, result.Records.Count); + Assert.Contains(result.Records, t => t.Values.Count == 1 && t.Values[0] == "v=spf1 -all"); + Assert.Contains(result.Records, t => t.Values.Count == 2 && t.Values[0] == "part1" && t.Values[1] == "part2"); + } + + [ConditionalFact(nameof(IsSupported))] + [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + public async Task ResolveCName_ReturnsCanonicalName() + { + await using LoopbackDnsServer server = LoopbackDnsServer.Start(); + using DnsResolver resolver = CreateResolver(server); + + string name = UniqueName("alias"); + server.AddResponse(name, DnsRecordType.CNAME, b => b + .Answer(DnsResponseBuilder.EncodeName("canonical.test"), ttl: 120)); + + DnsResult result = await resolver.ResolveCNameAsync(name); + + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + CNameRecord record = Assert.Single(result.Records); + Assert.Equal("canonical.test", record.CanonicalName); + } + + [ConditionalFact(nameof(IsSupported))] + [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + public async Task ResolvePtr_ReturnsName() + { + await using LoopbackDnsServer server = LoopbackDnsServer.Start(); + using DnsResolver resolver = CreateResolver(server); + + string name = $"1.0.0.10.in-addr.{UniqueName("arpa")}"; + server.AddResponse(name, DnsRecordType.PTR, b => b + .Answer(DnsResponseBuilder.EncodeName("host.test"), ttl: 120)); + + DnsResult result = await resolver.ResolvePtrAsync(name); + + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + PtrRecord record = Assert.Single(result.Records); + Assert.Equal("host.test", record.Name); + } + + [ConditionalFact(nameof(IsSupported))] + [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + public async Task ResolveNs_ReturnsRecords() + { + await using LoopbackDnsServer server = LoopbackDnsServer.Start(); + using DnsResolver resolver = CreateResolver(server); + + string name = UniqueName("ns"); + server.AddResponse(name, DnsRecordType.NS, b => b + .Answer(DnsResponseBuilder.EncodeName("ns1.test"), ttl: 120) + .Answer(DnsResponseBuilder.EncodeName("ns2.test"), ttl: 120)); + + DnsResult result = await resolver.ResolveNsAsync(name); + + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + Assert.Equal(2, result.Records.Count); + Assert.Contains(result.Records, n => n.Name == "ns1.test"); + Assert.Contains(result.Records, n => n.Name == "ns2.test"); + } + + // ---- Custom server endpoint handling ---- + + [ConditionalFact(nameof(IsSupported))] + [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + public async Task CustomServer_DefaultPortZero_IsAccepted() + { + await using LoopbackDnsServer server = LoopbackDnsServer.Start(); + // Port 0 means "use the default DNS port"; DnsQueryEx always queries port 53. + using DnsResolver resolver = new DnsResolver(new DnsResolverOptions + { + Servers = { new IPEndPoint(IPAddress.Loopback, 0) } + }); + + string name = UniqueName("port0"); + server.AddResponse(name, DnsRecordType.A, b => b.Answer(new byte[] { 10, 0, 0, 5 }, ttl: 120)); + server.AddResponse(name, DnsRecordType.AAAA, b => b.ResponseCode(DnsResponseCode.NxDomain)); + + DnsResult result = await resolver.ResolveAddressesAsync(name); + + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + AddressRecord record = Assert.Single(result.Records); + Assert.Equal("10.0.0.5", record.Address.ToString()); + } + + // ---- Cancellation while a query is in flight ---- + + [ConditionalFact(nameof(IsSupported))] + [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + public async Task ResolveAddresses_CancellationInFlight_Throws() + { + using SemaphoreSlim queryReceived = new(0, 1); + using ManualResetEventSlim serverCanContinue = new(false); + using CancellationTokenSource cts = new(); + + await using LoopbackDnsServer server = LoopbackDnsServer.Start(); + using DnsResolver resolver = CreateResolver(server); + + string name = UniqueName("cancel"); + server.AddRawResponse(name, DnsRecordType.A, queryId => + { + queryReceived.Release(); + // Hold the response until the test cancels and signals us to continue. + serverCanContinue.Wait(TimeSpan.FromSeconds(30)); + return DnsResponseBuilder.For(queryId, DnsResponseBuilder.EncodeName(name), DnsRecordType.A) + .Answer(new byte[] { 10, 0, 0, 1 }, ttl: 60) + .Build(); + }); + + // Query a single family so exactly one (blocked) UDP query is issued. + Task resolveTask = resolver.ResolveAddressesAsync(name, AddressFamily.InterNetwork, cts.Token); + + Assert.True(await queryReceived.WaitAsync(TimeSpan.FromSeconds(10))); + cts.Cancel(); + + await Assert.ThrowsAnyAsync(() => resolveTask); + + serverCanContinue.Set(); + } + } + + // The loopback DNS server binds the single machine-wide port 53, so all tests that + // use it must run sequentially. Placing them in this collection disables parallel + // execution between the test classes that opt into it. + [CollectionDefinition(nameof(DnsLoopbackTestCollection), DisableParallelization = true)] + public sealed class DnsLoopbackTestCollection { } +} diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs index 8307518af9ee3e..b47c117e78c893 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs @@ -213,6 +213,18 @@ public async Task DnsResolver_CustomServer_Port53_Works() Assert.NotEmpty(result.Records); } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + public async Task DnsResolver_CustomServer_NonStandardPort_ThrowsPlatformNotSupported() + { + // DnsQueryEx only supports custom DNS servers on the standard port 53. + DnsResolverOptions opts = new DnsResolverOptions + { + Servers = { new IPEndPoint(IPAddress.Loopback, 5353) } + }; + using DnsResolver r = new DnsResolver(opts); + await Assert.ThrowsAsync(() => r.ResolveAddressesAsync(TestHost)); + } + // ---- Reverse-arpa name building (covers both IPv4 and IPv6 paths used by ResolvePtr(IPAddress)) ---- [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResponseBuilder.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResponseBuilder.cs new file mode 100644 index 00000000000000..5d2141d6f7ad59 --- /dev/null +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResponseBuilder.cs @@ -0,0 +1,243 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Text; + +namespace System.Net.NameResolution.Tests +{ + // DNS record types used by the loopback test server. Values are the RFC-assigned TYPE codes. + internal enum DnsRecordType : ushort + { + A = 1, + NS = 2, + CNAME = 5, + SOA = 6, + PTR = 12, + MX = 15, + TXT = 16, + AAAA = 28, + SRV = 33, + } + + [Flags] + internal enum DnsHeaderFlags : ushort + { + None = 0, + Truncation = 0x0200, + RecursionDesired = 0x0100, + RecursionAvailable = 0x0080, + } + + /// + /// Fluent builder for constructing raw DNS response byte arrays in tests. + /// Self-contained: does not depend on any production DNS message types. + /// + internal sealed class DnsResponseBuilder + { + private readonly ushort _queryId; + private readonly byte[] _questionName; // wire-encoded question name (may be empty) + private readonly DnsRecordType _questionType; + + private DnsResponseCode _rcode; + private DnsHeaderFlags _extraFlags; + + private List<(byte[]? OwnerName, DnsRecordType Type, uint Ttl, byte[] Rdata)>? _answers; + private List<(byte[]? OwnerName, DnsRecordType Type, uint Ttl, byte[] Rdata)>? _authority; + private List<(byte[]? OwnerName, DnsRecordType Type, uint Ttl, byte[] Rdata)>? _additional; + + private int _questionCountOverride = -1; + private int _answerCountOverride = -1; + private int _authorityCountOverride = -1; + private int _additionalCountOverride = -1; + private bool _skipQuestion; + + private DnsResponseBuilder(ushort queryId, byte[] questionName, DnsRecordType questionType) + { + _queryId = queryId; + _questionName = questionName; + _questionType = questionType; + } + + public static DnsResponseBuilder For(ushort queryId, byte[] questionName, DnsRecordType questionType) + => new DnsResponseBuilder(queryId, questionName, questionType); + + public DnsResponseBuilder ResponseCode(DnsResponseCode rcode) + { + _rcode = rcode; + return this; + } + + public DnsResponseBuilder Truncated() + { + _extraFlags |= DnsHeaderFlags.Truncation; + return this; + } + + public DnsResponseBuilder Answer(byte[] rdata, uint ttl = 300) + => Answer(_questionType, rdata, ttl); + + public DnsResponseBuilder Answer(DnsRecordType type, byte[] rdata, uint ttl = 300) + { + _answers ??= new(); + _answers.Add((null, type, ttl, rdata)); + return this; + } + + public DnsResponseBuilder Answer(string ownerName, DnsRecordType type, byte[] rdata, uint ttl = 300) + { + _answers ??= new(); + _answers.Add((EncodeName(ownerName), type, ttl, rdata)); + return this; + } + + public DnsResponseBuilder Authority(string ownerName, DnsRecordType type, byte[] rdata, uint ttl = 60) + { + _authority ??= new(); + _authority.Add((EncodeName(ownerName), type, ttl, rdata)); + return this; + } + + public DnsResponseBuilder Additional(string ownerName, DnsRecordType type, byte[] rdata, uint ttl = 300) + { + _additional ??= new(); + _additional.Add((EncodeName(ownerName), type, ttl, rdata)); + return this; + } + + public DnsResponseBuilder OverrideQuestionCount(ushort count) { _questionCountOverride = count; return this; } + public DnsResponseBuilder OverrideAnswerCount(ushort count) { _answerCountOverride = count; return this; } + public DnsResponseBuilder OverrideAuthorityCount(ushort count) { _authorityCountOverride = count; return this; } + public DnsResponseBuilder OverrideAdditionalCount(ushort count) { _additionalCountOverride = count; return this; } + public DnsResponseBuilder SkipQuestion() { _skipQuestion = true; return this; } + + public byte[] Build() + { + int answerCount = _answers?.Count ?? 0; + int authorityCount = _authority?.Count ?? 0; + int additionalCount = _additional?.Count ?? 0; + bool writeQuestion = !_skipQuestion && _questionName.Length > 0; + + byte[] buf = new byte[4096]; + int offset = 0; + + // Header + BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(offset), _queryId); + ushort flags = (ushort)(0x8000 // QR (response) + | (ushort)(DnsHeaderFlags.RecursionDesired | DnsHeaderFlags.RecursionAvailable | _extraFlags) + | ((ushort)_rcode & 0xF)); + BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(offset + 2), flags); + BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(offset + 4), (ushort)(_questionCountOverride >= 0 ? _questionCountOverride : (writeQuestion ? 1 : 0))); + BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(offset + 6), (ushort)(_answerCountOverride >= 0 ? _answerCountOverride : answerCount)); + BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(offset + 8), (ushort)(_authorityCountOverride >= 0 ? _authorityCountOverride : authorityCount)); + BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(offset + 10), (ushort)(_additionalCountOverride >= 0 ? _additionalCountOverride : additionalCount)); + offset += 12; + + if (writeQuestion) + { + _questionName.CopyTo(buf.AsSpan(offset)); + offset += _questionName.Length; + BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(offset), (ushort)_questionType); + BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(offset + 2), 1); // class IN + offset += 4; + } + + WriteSection(buf, ref offset, _answers); + WriteSection(buf, ref offset, _authority); + WriteSection(buf, ref offset, _additional); + + return buf[..offset]; + } + + private void WriteSection(byte[] buf, ref int offset, + List<(byte[]? OwnerName, DnsRecordType Type, uint Ttl, byte[] Rdata)>? records) + { + if (records is null) + { + return; + } + + foreach ((byte[]? ownerName, DnsRecordType type, uint ttl, byte[] rdata) in records) + { + byte[] name = ownerName ?? _questionName; + name.CopyTo(buf.AsSpan(offset)); + offset += name.Length; + BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(offset), (ushort)type); + BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(offset + 2), 1); // class IN + BinaryPrimitives.WriteUInt32BigEndian(buf.AsSpan(offset + 4), ttl); + BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(offset + 8), (ushort)rdata.Length); + offset += 10; + rdata.CopyTo(buf.AsSpan(offset)); + offset += rdata.Length; + } + } + + internal static byte[] EncodeName(string name) + { + if (string.IsNullOrEmpty(name) || name == ".") + { + return new byte[] { 0 }; + } + + List bytes = new(); + foreach (string label in name.Split('.', StringSplitOptions.RemoveEmptyEntries)) + { + byte[] labelBytes = Encoding.ASCII.GetBytes(label); + bytes.Add((byte)labelBytes.Length); + bytes.AddRange(labelBytes); + } + bytes.Add(0); + return bytes.ToArray(); + } + + internal static byte[] BuildSoaRdata(string soaName, uint minTtl) + { + byte[] mname = EncodeName("ns." + soaName); + byte[] rname = EncodeName("admin." + soaName); + byte[] rdata = new byte[mname.Length + rname.Length + 20]; + mname.CopyTo(rdata, 0); + rname.CopyTo(rdata, mname.Length); + int fixedStart = mname.Length + rname.Length; + BinaryPrimitives.WriteUInt32BigEndian(rdata.AsSpan(fixedStart), 2024010101); // serial + BinaryPrimitives.WriteUInt32BigEndian(rdata.AsSpan(fixedStart + 4), 3600); // refresh + BinaryPrimitives.WriteUInt32BigEndian(rdata.AsSpan(fixedStart + 8), 900); // retry + BinaryPrimitives.WriteUInt32BigEndian(rdata.AsSpan(fixedStart + 12), 604800); // expire + BinaryPrimitives.WriteUInt32BigEndian(rdata.AsSpan(fixedStart + 16), minTtl); // minimum + return rdata; + } + + internal static byte[] BuildSrvRdata(ushort priority, ushort weight, ushort port, string target) + { + byte[] targetBytes = EncodeName(target); + byte[] rdata = new byte[6 + targetBytes.Length]; + BinaryPrimitives.WriteUInt16BigEndian(rdata, priority); + BinaryPrimitives.WriteUInt16BigEndian(rdata.AsSpan(2), weight); + BinaryPrimitives.WriteUInt16BigEndian(rdata.AsSpan(4), port); + targetBytes.CopyTo(rdata, 6); + return rdata; + } + + internal static byte[] BuildMxRdata(ushort preference, string exchange) + { + byte[] exchangeBytes = EncodeName(exchange); + byte[] rdata = new byte[2 + exchangeBytes.Length]; + BinaryPrimitives.WriteUInt16BigEndian(rdata, preference); + exchangeBytes.CopyTo(rdata, 2); + return rdata; + } + + // A single character-string (length-prefixed) TXT value. + internal static byte[] BuildTxtRdata(params string[] values) + { + List rdata = new(); + foreach (string value in values) + { + byte[] valueBytes = Encoding.ASCII.GetBytes(value); + rdata.Add((byte)valueBytes.Length); + rdata.AddRange(valueBytes); + } + return rdata.ToArray(); + } + } +} diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/LoopbackDnsServer.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/LoopbackDnsServer.cs new file mode 100644 index 00000000000000..5d399f66b2464c --- /dev/null +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/LoopbackDnsServer.cs @@ -0,0 +1,275 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.DotNet.XUnitExtensions; + +namespace System.Net.NameResolution.Tests +{ + /// + /// A minimal in-process DNS server for testing. Listens on the loopback DNS port (53) + /// and responds with preconfigured answers based on the query name and type. + /// Self-contained: does not depend on any production DNS message types. + /// + /// + /// Windows' DnsQueryEx only ever contacts custom DNS servers on the standard + /// port 53 (the sockaddr port field must be 0), so the loopback server must bind 53. + /// Binding a privileged-looking low port does not require elevation on Windows, but + /// the port may already be in use (e.g. a local DNS service), in which case + /// throws so the test is skipped + /// rather than failed. + /// + internal sealed class LoopbackDnsServer : IAsyncDisposable + { + // DnsQueryEx always queries DNS servers on the standard port 53. + public const int DnsPort = 53; + + private readonly UdpClient _udp; + private readonly TcpListener _tcp; + private readonly CancellationTokenSource _cts = new(); + private readonly Task _udpListenTask; + private readonly Task _tcpListenTask; + private readonly Dictionary<(string Name, DnsRecordType Type), ResponseBuilder> _responses = new(); + private int _requestCount; + + public IPEndPoint EndPoint { get; } + + public int RequestCount => _requestCount; + + public int TcpRequestCount { get; private set; } + + private LoopbackDnsServer(UdpClient udp, TcpListener tcp, IPEndPoint endPoint) + { + _udp = udp; + _tcp = tcp; + EndPoint = endPoint; + _udpListenTask = ListenUdpAsync(_cts.Token); + _tcpListenTask = ListenTcpAsync(_cts.Token); + } + + public static LoopbackDnsServer Start() + { + UdpClient udp; + try + { + udp = new UdpClient(new IPEndPoint(IPAddress.Loopback, DnsPort)); + } + catch (SocketException ex) + { + throw new SkipTestException( + $"Unable to bind loopback DNS port {DnsPort}; another DNS server may be running ({ex.SocketErrorCode})."); + } + + IPEndPoint ep = (IPEndPoint)udp.Client.LocalEndPoint!; + TcpListener tcp = new(IPAddress.Loopback, ep.Port); + try + { + tcp.Start(); + } + catch (SocketException ex) + { + udp.Dispose(); + throw new SkipTestException( + $"Unable to bind loopback DNS TCP port {DnsPort}; another DNS server may be running ({ex.SocketErrorCode})."); + } + + return new LoopbackDnsServer(udp, tcp, ep); + } + + public void AddResponse(string name, DnsRecordType type, Func configure) + { + _responses[(name.ToLowerInvariant(), type)] = (queryId, qName, _) => + configure(DnsResponseBuilder.For(queryId, qName, type)).Build(); + } + + public void AddRawResponse(string name, DnsRecordType type, Func rawFactory) + { + _responses[(name.ToLowerInvariant(), type)] = (queryId, _, _) => rawFactory(queryId); + } + + public delegate byte[] ResponseBuilder(ushort queryId, byte[] questionName, bool isTcp); + + private async Task ListenUdpAsync(CancellationToken ct) + { + try + { + while (!ct.IsCancellationRequested) + { + UdpReceiveResult result = await _udp.ReceiveAsync(ct); + Interlocked.Increment(ref _requestCount); + + byte[] response = ProcessQuery(result.Buffer); + if (response.Length > 0) + { + await _udp.SendAsync(response, result.RemoteEndPoint, ct); + } + } + } + catch (OperationCanceledException) { } + catch (ObjectDisposedException) { } + } + + private async Task ListenTcpAsync(CancellationToken ct) + { + try + { + while (!ct.IsCancellationRequested) + { + TcpClient client = await _tcp.AcceptTcpClientAsync(ct); + _ = HandleTcpClientAsync(client, ct); + } + } + catch (OperationCanceledException) { } + catch (ObjectDisposedException) { } + } + + private async Task HandleTcpClientAsync(TcpClient client, CancellationToken ct) + { + try + { + using (client) + { + NetworkStream stream = client.GetStream(); + + byte[] lengthBuf = new byte[2]; + if (!await ReadExactlyAsync(stream, lengthBuf, ct)) + { + return; + } + + int queryLength = BinaryPrimitives.ReadUInt16BigEndian(lengthBuf); + byte[] query = new byte[queryLength]; + if (!await ReadExactlyAsync(stream, query, ct)) + { + return; + } + + Interlocked.Increment(ref _requestCount); + TcpRequestCount++; + + byte[] response = ProcessQuery(query, isTcp: true); + if (response.Length > 0) + { + byte[] responseLengthBuf = new byte[2]; + BinaryPrimitives.WriteUInt16BigEndian(responseLengthBuf, (ushort)response.Length); + await stream.WriteAsync(responseLengthBuf, ct); + await stream.WriteAsync(response, ct); + } + } + } + catch (OperationCanceledException) { } + catch (ObjectDisposedException) { } + catch (IOException) { } + } + + private static async Task ReadExactlyAsync(NetworkStream stream, byte[] buffer, CancellationToken ct) + { + int read = 0; + while (read < buffer.Length) + { + int n = await stream.ReadAsync(buffer.AsMemory(read), ct); + if (n == 0) + { + return false; + } + read += n; + } + return true; + } + + private byte[] ProcessQuery(byte[] query, bool isTcp = false) + { + if (query.Length < 12) + { + return []; + } + + ushort queryId = BinaryPrimitives.ReadUInt16BigEndian(query); + ushort qdCount = BinaryPrimitives.ReadUInt16BigEndian(query.AsSpan(4)); + + if (qdCount < 1) + { + return DnsResponseBuilder.For(queryId, [], 0) + .ResponseCode(DnsResponseCode.FormatError) + .SkipQuestion() + .Build(); + } + + int pos = 12; + int nameStart = pos; + + while (pos < query.Length) + { + byte b = query[pos]; + if (b == 0) { pos++; break; } + if ((b & 0xC0) == 0xC0) { pos += 2; break; } + pos += 1 + b; + } + + byte[] questionName = query[nameStart..pos]; + + if (pos + 4 > query.Length) + { + return DnsResponseBuilder.For(queryId, questionName, 0) + .ResponseCode(DnsResponseCode.FormatError) + .Build(); + } + + DnsRecordType qType = (DnsRecordType)BinaryPrimitives.ReadUInt16BigEndian(query.AsSpan(pos)); + string nameStr = DecodeName(query, nameStart); + + if (_responses.TryGetValue((nameStr.ToLowerInvariant(), qType), out ResponseBuilder? builder)) + { + return builder(queryId, questionName, isTcp); + } + + // Default: NXDOMAIN + return DnsResponseBuilder.For(queryId, questionName, qType) + .ResponseCode(DnsResponseCode.NxDomain) + .Build(); + } + + private static string DecodeName(byte[] message, int offset) + { + StringBuilder sb = new(); + int pos = offset; + while (pos < message.Length) + { + byte len = message[pos]; + if (len == 0) + { + break; + } + if ((len & 0xC0) == 0xC0) + { + pos = ((len & 0x3F) << 8) | message[pos + 1]; + continue; + } + pos++; + if (sb.Length > 0) + { + sb.Append('.'); + } + sb.Append(Encoding.ASCII.GetString(message, pos, len)); + pos += len; + } + return sb.ToString(); + } + + public async ValueTask DisposeAsync() + { + _cts.Cancel(); + _udp.Dispose(); + _tcp.Stop(); + try { await _udpListenTask; } catch { } + try { await _tcpListenTask; } catch { } + _cts.Dispose(); + } + } +} diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/System.Net.NameResolution.Functional.Tests.csproj b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/System.Net.NameResolution.Functional.Tests.csproj index bc260f9daf3b20..02d5851e2c32f5 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/System.Net.NameResolution.Functional.Tests.csproj +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/System.Net.NameResolution.Functional.Tests.csproj @@ -10,6 +10,9 @@ + + + From a589fadb1690d7a35ee657453c14e2fed20ea296 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Thu, 11 Jun 2026 13:36:41 +0200 Subject: [PATCH 04/19] Polish tests --- .../DnsResolverLoopbackTest.cs | 244 +++++++----------- .../tests/FunctionalTests/DnsResolverTest.cs | 7 +- .../FunctionalTests/LoopbackDnsServer.cs | 83 +++--- 3 files changed, 159 insertions(+), 175 deletions(-) diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs index e713e60bdda6dc..0f240ff92777db 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs @@ -8,6 +8,27 @@ namespace System.Net.NameResolution.Tests { + public class WindowsLoopbackServer : IAsyncDisposable + { + private LoopbackDnsServer _server; + + public WindowsLoopbackServer() + { + _server = LoopbackDnsServer.Start(); + } + + internal LoopbackDnsServer Server => _server; + + public async ValueTask DisposeAsync() + { + if (_server != null) + { + await _server.DisposeAsync(); + _server = null; + } + } + } + // Deterministic DnsResolver tests driven by an in-process loopback DNS server. // // On Windows, DnsQueryEx only ever contacts custom DNS servers on the standard @@ -18,11 +39,11 @@ namespace System.Net.NameResolution.Tests // // These tests cover the record-parsing and response-handling behavior that the // OuterLoop tests in DnsResolverTest.cs cannot exercise deterministically. - [Collection(nameof(DnsLoopbackTestCollection))] - public class DnsResolverLoopbackTest + [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + [Collection(nameof(DisableParallelization))] + [PlatformSpecific(TestPlatforms.Windows)] + public class DnsResolverLoopbackTest : IClassFixture { - public static bool IsSupported => PlatformDetection.IsWindows; - private static DnsResolver CreateResolver(LoopbackDnsServer server) => new DnsResolver(new DnsResolverOptions { Servers = { server.EndPoint } }); @@ -30,20 +51,27 @@ private static DnsResolver CreateResolver(LoopbackDnsServer server) // previous test run can satisfy the query without reaching the loopback server. private static string UniqueName(string label) => $"{label}-{Guid.NewGuid():N}.test"; + LoopbackDnsServer _server; + DnsResolver? _resolver; + + public DnsResolverLoopbackTest(WindowsLoopbackServer fixture) + { + _server = fixture.Server; + _server.ClearResponses(); + } + + internal DnsResolver Resolver => _resolver ??= CreateResolver(_server); + // ---- Address resolution ---- - [ConditionalFact(nameof(IsSupported))] - [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + [Fact] public async Task ResolveAddresses_Unspecified_ReturnsBothV4AndV6() { - await using LoopbackDnsServer server = LoopbackDnsServer.Start(); - using DnsResolver resolver = CreateResolver(server); - string name = UniqueName("host"); - server.AddResponse(name, DnsRecordType.A, b => b.Answer(new byte[] { 10, 0, 0, 1 }, ttl: 120)); - server.AddResponse(name, DnsRecordType.AAAA, b => b.Answer(IPAddress.Parse("fd00::1").GetAddressBytes(), ttl: 60)); + _server.AddResponse(name, DnsRecordType.A, b => b.Answer(new byte[] { 10, 0, 0, 1 }, ttl: 120)); + _server.AddResponse(name, DnsRecordType.AAAA, b => b.Answer(IPAddress.Parse("fd00::1").GetAddressBytes(), ttl: 60)); - DnsResult result = await resolver.ResolveAddressesAsync(name); + DnsResult result = await Resolver.ResolveAddressesAsync(name); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); Assert.Equal(2, result.Records.Count); @@ -51,18 +79,14 @@ public async Task ResolveAddresses_Unspecified_ReturnsBothV4AndV6() Assert.Contains(result.Records, a => a.Address.ToString() == "fd00::1"); } - [ConditionalFact(nameof(IsSupported))] - [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + [Fact] public async Task ResolveAddresses_IPv4Only_ReturnsOnlyV4() { - await using LoopbackDnsServer server = LoopbackDnsServer.Start(); - using DnsResolver resolver = CreateResolver(server); - string name = UniqueName("v4"); - server.AddResponse(name, DnsRecordType.A, b => b.Answer(new byte[] { 10, 0, 0, 2 }, ttl: 300)); - server.AddResponse(name, DnsRecordType.AAAA, b => b.ResponseCode(DnsResponseCode.NxDomain)); + _server.AddResponse(name, DnsRecordType.A, b => b.Answer(new byte[] { 10, 0, 0, 2 }, ttl: 300)); + _server.AddResponse(name, DnsRecordType.AAAA, b => b.ResponseCode(DnsResponseCode.NxDomain)); - DnsResult result = await resolver.ResolveAddressesAsync(name); + DnsResult result = await Resolver.ResolveAddressesAsync(name); // A succeeds, AAAA returns NXDOMAIN — overall is success because we got addresses. Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); @@ -70,53 +94,41 @@ public async Task ResolveAddresses_IPv4Only_ReturnsOnlyV4() Assert.Equal("10.0.0.2", record.Address.ToString()); } - [ConditionalFact(nameof(IsSupported))] - [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + [Fact] public async Task ResolveAddresses_IPv6Only_ReturnsOnlyV6() { - await using LoopbackDnsServer server = LoopbackDnsServer.Start(); - using DnsResolver resolver = CreateResolver(server); - string name = UniqueName("v6"); - server.AddResponse(name, DnsRecordType.A, b => b.ResponseCode(DnsResponseCode.NxDomain)); - server.AddResponse(name, DnsRecordType.AAAA, b => b.Answer(IPAddress.Parse("fd00::1").GetAddressBytes(), ttl: 60)); + _server.AddResponse(name, DnsRecordType.A, b => b.ResponseCode(DnsResponseCode.NxDomain)); + _server.AddResponse(name, DnsRecordType.AAAA, b => b.Answer(IPAddress.Parse("fd00::1").GetAddressBytes(), ttl: 60)); - DnsResult result = await resolver.ResolveAddressesAsync(name); + DnsResult result = await Resolver.ResolveAddressesAsync(name); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); AddressRecord record = Assert.Single(result.Records); Assert.Equal("fd00::1", record.Address.ToString()); } - [ConditionalFact(nameof(IsSupported))] - [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + [Fact] public async Task ResolveAddresses_AddressFamilyV4_QueriesOnlyA() { - await using LoopbackDnsServer server = LoopbackDnsServer.Start(); - using DnsResolver resolver = CreateResolver(server); - string name = UniqueName("famv4"); - server.AddResponse(name, DnsRecordType.A, b => b.Answer(new byte[] { 192, 0, 2, 7 }, ttl: 200)); + _server.AddResponse(name, DnsRecordType.A, b => b.Answer(new byte[] { 192, 0, 2, 7 }, ttl: 200)); - DnsResult result = await resolver.ResolveAddressesAsync(name, AddressFamily.InterNetwork); + DnsResult result = await Resolver.ResolveAddressesAsync(name, AddressFamily.InterNetwork); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); AddressRecord record = Assert.Single(result.Records); Assert.Equal("192.0.2.7", record.Address.ToString()); } - [ConditionalFact(nameof(IsSupported))] - [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + [Fact] public async Task ResolveAddresses_HasTtl() { - await using LoopbackDnsServer server = LoopbackDnsServer.Start(); - using DnsResolver resolver = CreateResolver(server); - string name = UniqueName("ttl"); - server.AddResponse(name, DnsRecordType.A, b => b.Answer(new byte[] { 10, 0, 0, 1 }, ttl: 120)); - server.AddResponse(name, DnsRecordType.AAAA, b => b.ResponseCode(DnsResponseCode.NxDomain)); + _server.AddResponse(name, DnsRecordType.A, b => b.Answer(new byte[] { 10, 0, 0, 1 }, ttl: 120)); + _server.AddResponse(name, DnsRecordType.AAAA, b => b.ResponseCode(DnsResponseCode.NxDomain)); - DnsResult result = await resolver.ResolveAddressesAsync(name); + DnsResult result = await Resolver.ResolveAddressesAsync(name); AddressRecord record = Assert.Single(result.Records); // The TTL we sent (120s) should be preserved (custom-server queries bypass the OS cache). @@ -124,77 +136,65 @@ public async Task ResolveAddresses_HasTtl() $"Unexpected TTL: {record.Ttl}"); } - [ConditionalFact(nameof(IsSupported))] - [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + [Fact] public async Task ResolveAddresses_Nxdomain_ReturnsNxDomain() { - await using LoopbackDnsServer server = LoopbackDnsServer.Start(); - using DnsResolver resolver = CreateResolver(server); - string name = UniqueName("missing"); byte[] soaRdata = DnsResponseBuilder.BuildSoaRdata("test", 120); - server.AddResponse(name, DnsRecordType.A, b => b + _server.AddResponse(name, DnsRecordType.A, b => b .ResponseCode(DnsResponseCode.NxDomain) .Authority("test", DnsRecordType.SOA, soaRdata, ttl: 120)); - server.AddResponse(name, DnsRecordType.AAAA, b => b + _server.AddResponse(name, DnsRecordType.AAAA, b => b .ResponseCode(DnsResponseCode.NxDomain) .Authority("test", DnsRecordType.SOA, soaRdata, ttl: 120)); - DnsResult result = await resolver.ResolveAddressesAsync(name); + DnsResult result = await Resolver.ResolveAddressesAsync(name); Assert.Equal(DnsResponseCode.NxDomain, result.ResponseCode); Assert.Empty(result.Records); } - [ConditionalFact(nameof(IsSupported))] - [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + [Fact] public async Task ResolveAddresses_NoData_ReturnsNoErrorWithEmptyRecords() { - await using LoopbackDnsServer server = LoopbackDnsServer.Start(); - using DnsResolver resolver = CreateResolver(server); - string name = UniqueName("nodata"); byte[] soaRdata = DnsResponseBuilder.BuildSoaRdata("test", 30); - server.AddResponse(name, DnsRecordType.A, b => b + _server.AddResponse(name, DnsRecordType.A, b => b .Authority("test", DnsRecordType.SOA, soaRdata, ttl: 30)); - server.AddResponse(name, DnsRecordType.AAAA, b => b + _server.AddResponse(name, DnsRecordType.AAAA, b => b .Authority("test", DnsRecordType.SOA, soaRdata, ttl: 30)); // The name exists but has no A/AAAA records → NODATA for both queries. - DnsResult result = await resolver.ResolveAddressesAsync(name); + DnsResult result = await Resolver.ResolveAddressesAsync(name); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); Assert.Empty(result.Records); } - [ConditionalFact(nameof(IsSupported))] - [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + [Fact] public async Task ResolveAddresses_NoData_And_Nxdomain_AreDistinguishable() { - await using LoopbackDnsServer server = LoopbackDnsServer.Start(); - using DnsResolver resolver = CreateResolver(server); - string nodataName = UniqueName("nodata"); byte[] soaRdata = DnsResponseBuilder.BuildSoaRdata("test", 30); - server.AddResponse(nodataName, DnsRecordType.A, b => b + _server.AddResponse(nodataName, DnsRecordType.A, b => b .Authority("test", DnsRecordType.SOA, soaRdata, ttl: 30)); - server.AddResponse(nodataName, DnsRecordType.AAAA, b => b + _server.AddResponse(nodataName, DnsRecordType.AAAA, b => b .Authority("test", DnsRecordType.SOA, soaRdata, ttl: 30)); string missingName = UniqueName("missing"); byte[] nxSoaRdata = DnsResponseBuilder.BuildSoaRdata("test", 120); - server.AddResponse(missingName, DnsRecordType.A, b => b + _server.AddResponse(missingName, DnsRecordType.A, b => b .ResponseCode(DnsResponseCode.NxDomain) .Authority("test", DnsRecordType.SOA, nxSoaRdata, ttl: 120)); - server.AddResponse(missingName, DnsRecordType.AAAA, b => b + _server.AddResponse(missingName, DnsRecordType.AAAA, b => b .ResponseCode(DnsResponseCode.NxDomain) .Authority("test", DnsRecordType.SOA, nxSoaRdata, ttl: 120)); - DnsResult nodata = await resolver.ResolveAddressesAsync(nodataName); + DnsResult nodata = await Resolver.ResolveAddressesAsync(nodataName); Assert.Equal(DnsResponseCode.NoError, nodata.ResponseCode); Assert.Empty(nodata.Records); - DnsResult nxdomain = await resolver.ResolveAddressesAsync(missingName); + DnsResult nxdomain = await Resolver.ResolveAddressesAsync(missingName); Assert.Equal(DnsResponseCode.NxDomain, nxdomain.ResponseCode); Assert.Empty(nxdomain.Records); @@ -203,19 +203,15 @@ public async Task ResolveAddresses_NoData_And_Nxdomain_AreDistinguishable() // ---- SRV ---- - [ConditionalFact(nameof(IsSupported))] - [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + [Fact] public async Task ResolveSrv_ReturnsRecords() { - await using LoopbackDnsServer server = LoopbackDnsServer.Start(); - using DnsResolver resolver = CreateResolver(server); - string name = $"_http._tcp.{UniqueName("svc")}"; - server.AddResponse(name, DnsRecordType.SRV, b => b + _server.AddResponse(name, DnsRecordType.SRV, b => b .Answer(DnsResponseBuilder.BuildSrvRdata(10, 100, 8080, "node1.test"), ttl: 120) .Answer(DnsResponseBuilder.BuildSrvRdata(20, 50, 8081, "node2.test"), ttl: 120)); - DnsResult result = await resolver.ResolveSrvAsync(name); + DnsResult result = await Resolver.ResolveSrvAsync(name); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); Assert.Equal(2, result.Records.Count); @@ -230,22 +226,18 @@ public async Task ResolveSrv_ReturnsRecords() Assert.Equal((ushort)20, s2.Priority); } - [ConditionalFact(nameof(IsSupported))] - [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + [Fact] public async Task ResolveSrv_IncludesAdditionalAddresses() { - await using LoopbackDnsServer server = LoopbackDnsServer.Start(); - using DnsResolver resolver = CreateResolver(server); - string name = $"_http._tcp.{UniqueName("svc")}"; - server.AddResponse(name, DnsRecordType.SRV, b => b + _server.AddResponse(name, DnsRecordType.SRV, b => b .Answer(DnsResponseBuilder.BuildSrvRdata(10, 100, 8080, "node1.test"), ttl: 120) .Answer(DnsResponseBuilder.BuildSrvRdata(20, 50, 8081, "node2.test"), ttl: 120) .Additional("node1.test", DnsRecordType.A, new byte[] { 10, 0, 0, 10 }, ttl: 120) .Additional("node2.test", DnsRecordType.A, new byte[] { 10, 0, 0, 11 }, ttl: 120) .Additional("node2.test", DnsRecordType.AAAA, IPAddress.Parse("fd00::11").GetAddressBytes(), ttl: 120)); - DnsResult result = await resolver.ResolveSrvAsync(name); + DnsResult result = await Resolver.ResolveSrvAsync(name); SrvRecord s1 = Assert.Single(result.Records, s => s.Target == "node1.test"); Assert.NotNull(s1.Addresses); @@ -257,18 +249,14 @@ public async Task ResolveSrv_IncludesAdditionalAddresses() Assert.Equal(2, s2.Addresses.Count); } - [ConditionalFact(nameof(IsSupported))] - [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + [Fact] public async Task ResolveSrv_NoAdditionalAddresses() { - await using LoopbackDnsServer server = LoopbackDnsServer.Start(); - using DnsResolver resolver = CreateResolver(server); - string name = $"_noadd._tcp.{UniqueName("svc")}"; - server.AddResponse(name, DnsRecordType.SRV, b => b + _server.AddResponse(name, DnsRecordType.SRV, b => b .Answer(DnsResponseBuilder.BuildSrvRdata(10, 100, 9090, "noaddr.test"), ttl: 60)); - DnsResult result = await resolver.ResolveSrvAsync(name); + DnsResult result = await Resolver.ResolveSrvAsync(name); SrvRecord record = Assert.Single(result.Records); Assert.Equal("noaddr.test", record.Target); @@ -277,19 +265,15 @@ public async Task ResolveSrv_NoAdditionalAddresses() // ---- MX / TXT / CNAME / PTR / NS ---- - [ConditionalFact(nameof(IsSupported))] - [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + [Fact] public async Task ResolveMx_ReturnsRecords() { - await using LoopbackDnsServer server = LoopbackDnsServer.Start(); - using DnsResolver resolver = CreateResolver(server); - string name = UniqueName("mx"); - server.AddResponse(name, DnsRecordType.MX, b => b + _server.AddResponse(name, DnsRecordType.MX, b => b .Answer(DnsResponseBuilder.BuildMxRdata(10, "mail1.test"), ttl: 120) .Answer(DnsResponseBuilder.BuildMxRdata(20, "mail2.test"), ttl: 120)); - DnsResult result = await resolver.ResolveMxAsync(name); + DnsResult result = await Resolver.ResolveMxAsync(name); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); Assert.Equal(2, result.Records.Count); @@ -299,19 +283,15 @@ public async Task ResolveMx_ReturnsRecords() Assert.Single(result.Records, m => m.Exchange == "mail2.test" && m.Preference == 20); } - [ConditionalFact(nameof(IsSupported))] - [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + [Fact] public async Task ResolveTxt_ReturnsValues() { - await using LoopbackDnsServer server = LoopbackDnsServer.Start(); - using DnsResolver resolver = CreateResolver(server); - string name = UniqueName("txt"); - server.AddResponse(name, DnsRecordType.TXT, b => b + _server.AddResponse(name, DnsRecordType.TXT, b => b .Answer(DnsResponseBuilder.BuildTxtRdata("v=spf1 -all"), ttl: 120) .Answer(DnsResponseBuilder.BuildTxtRdata("part1", "part2"), ttl: 120)); - DnsResult result = await resolver.ResolveTxtAsync(name); + DnsResult result = await Resolver.ResolveTxtAsync(name); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); Assert.Equal(2, result.Records.Count); @@ -319,55 +299,43 @@ public async Task ResolveTxt_ReturnsValues() Assert.Contains(result.Records, t => t.Values.Count == 2 && t.Values[0] == "part1" && t.Values[1] == "part2"); } - [ConditionalFact(nameof(IsSupported))] - [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + [Fact] public async Task ResolveCName_ReturnsCanonicalName() { - await using LoopbackDnsServer server = LoopbackDnsServer.Start(); - using DnsResolver resolver = CreateResolver(server); - string name = UniqueName("alias"); - server.AddResponse(name, DnsRecordType.CNAME, b => b + _server.AddResponse(name, DnsRecordType.CNAME, b => b .Answer(DnsResponseBuilder.EncodeName("canonical.test"), ttl: 120)); - DnsResult result = await resolver.ResolveCNameAsync(name); + DnsResult result = await Resolver.ResolveCNameAsync(name); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); CNameRecord record = Assert.Single(result.Records); Assert.Equal("canonical.test", record.CanonicalName); } - [ConditionalFact(nameof(IsSupported))] - [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + [Fact] public async Task ResolvePtr_ReturnsName() { - await using LoopbackDnsServer server = LoopbackDnsServer.Start(); - using DnsResolver resolver = CreateResolver(server); - string name = $"1.0.0.10.in-addr.{UniqueName("arpa")}"; - server.AddResponse(name, DnsRecordType.PTR, b => b + _server.AddResponse(name, DnsRecordType.PTR, b => b .Answer(DnsResponseBuilder.EncodeName("host.test"), ttl: 120)); - DnsResult result = await resolver.ResolvePtrAsync(name); + DnsResult result = await Resolver.ResolvePtrAsync(name); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); PtrRecord record = Assert.Single(result.Records); Assert.Equal("host.test", record.Name); } - [ConditionalFact(nameof(IsSupported))] - [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + [Fact] public async Task ResolveNs_ReturnsRecords() { - await using LoopbackDnsServer server = LoopbackDnsServer.Start(); - using DnsResolver resolver = CreateResolver(server); - string name = UniqueName("ns"); - server.AddResponse(name, DnsRecordType.NS, b => b + _server.AddResponse(name, DnsRecordType.NS, b => b .Answer(DnsResponseBuilder.EncodeName("ns1.test"), ttl: 120) .Answer(DnsResponseBuilder.EncodeName("ns2.test"), ttl: 120)); - DnsResult result = await resolver.ResolveNsAsync(name); + DnsResult result = await Resolver.ResolveNsAsync(name); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); Assert.Equal(2, result.Records.Count); @@ -377,11 +345,9 @@ public async Task ResolveNs_ReturnsRecords() // ---- Custom server endpoint handling ---- - [ConditionalFact(nameof(IsSupported))] - [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + [Fact] public async Task CustomServer_DefaultPortZero_IsAccepted() { - await using LoopbackDnsServer server = LoopbackDnsServer.Start(); // Port 0 means "use the default DNS port"; DnsQueryEx always queries port 53. using DnsResolver resolver = new DnsResolver(new DnsResolverOptions { @@ -389,8 +355,8 @@ public async Task CustomServer_DefaultPortZero_IsAccepted() }); string name = UniqueName("port0"); - server.AddResponse(name, DnsRecordType.A, b => b.Answer(new byte[] { 10, 0, 0, 5 }, ttl: 120)); - server.AddResponse(name, DnsRecordType.AAAA, b => b.ResponseCode(DnsResponseCode.NxDomain)); + _server.AddResponse(name, DnsRecordType.A, b => b.Answer(new byte[] { 10, 0, 0, 5 }, ttl: 120)); + _server.AddResponse(name, DnsRecordType.AAAA, b => b.ResponseCode(DnsResponseCode.NxDomain)); DnsResult result = await resolver.ResolveAddressesAsync(name); @@ -401,19 +367,15 @@ public async Task CustomServer_DefaultPortZero_IsAccepted() // ---- Cancellation while a query is in flight ---- - [ConditionalFact(nameof(IsSupported))] - [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] + [Fact] public async Task ResolveAddresses_CancellationInFlight_Throws() { using SemaphoreSlim queryReceived = new(0, 1); using ManualResetEventSlim serverCanContinue = new(false); using CancellationTokenSource cts = new(); - await using LoopbackDnsServer server = LoopbackDnsServer.Start(); - using DnsResolver resolver = CreateResolver(server); - string name = UniqueName("cancel"); - server.AddRawResponse(name, DnsRecordType.A, queryId => + _server.AddRawResponse(name, DnsRecordType.A, queryId => { queryReceived.Release(); // Hold the response until the test cancels and signals us to continue. @@ -424,7 +386,7 @@ public async Task ResolveAddresses_CancellationInFlight_Throws() }); // Query a single family so exactly one (blocked) UDP query is issued. - Task resolveTask = resolver.ResolveAddressesAsync(name, AddressFamily.InterNetwork, cts.Token); + Task resolveTask = Resolver.ResolveAddressesAsync(name, AddressFamily.InterNetwork, cts.Token); Assert.True(await queryReceived.WaitAsync(TimeSpan.FromSeconds(10))); cts.Cancel(); @@ -434,10 +396,4 @@ public async Task ResolveAddresses_CancellationInFlight_Throws() serverCanContinue.Set(); } } - - // The loopback DNS server binds the single machine-wide port 53, so all tests that - // use it must run sequentially. Placing them in this collection disables parallel - // execution between the test classes that opt into it. - [CollectionDefinition(nameof(DnsLoopbackTestCollection), DisableParallelization = true)] - public sealed class DnsLoopbackTestCollection { } } diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs index b47c117e78c893..32e6bdf1aea582 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs @@ -4,6 +4,7 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; +using System.Linq; using Xunit; namespace System.Net.NameResolution.Tests @@ -203,9 +204,13 @@ public async Task Static_Dns_ResolveAddressesAsync_Works() [OuterLoop] public async Task DnsResolver_CustomServer_Port53_Works() { + IPAddress dnsAddress = System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces() + .SelectMany(ni => ni.GetIPProperties().DnsAddresses) + .FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetwork); + DnsResolverOptions opts = new DnsResolverOptions { - Servers = { new IPEndPoint(IPAddress.Parse("8.8.8.8"), 53) } + Servers = { new IPEndPoint(dnsAddress, 53) } }; using DnsResolver r = new DnsResolver(opts); DnsResult result = await r.ResolveAddressesAsync(TestHost); diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/LoopbackDnsServer.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/LoopbackDnsServer.cs index 5d399f66b2464c..e871a80f76ec99 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/LoopbackDnsServer.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/LoopbackDnsServer.cs @@ -2,8 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers.Binary; +using System.Diagnostics; using System.Collections.Generic; -using System.IO; using System.Net.Sockets; using System.Text; using System.Threading; @@ -27,11 +27,10 @@ namespace System.Net.NameResolution.Tests /// internal sealed class LoopbackDnsServer : IAsyncDisposable { - // DnsQueryEx always queries DNS servers on the standard port 53. public const int DnsPort = 53; - private readonly UdpClient _udp; - private readonly TcpListener _tcp; + private readonly Socket _udp; + private readonly Socket _tcp; private readonly CancellationTokenSource _cts = new(); private readonly Task _udpListenTask; private readonly Task _tcpListenTask; @@ -44,7 +43,7 @@ internal sealed class LoopbackDnsServer : IAsyncDisposable public int TcpRequestCount { get; private set; } - private LoopbackDnsServer(UdpClient udp, TcpListener tcp, IPEndPoint endPoint) + private LoopbackDnsServer(Socket udp, Socket tcp, IPEndPoint endPoint) { _udp = udp; _tcp = tcp; @@ -55,26 +54,35 @@ private LoopbackDnsServer(UdpClient udp, TcpListener tcp, IPEndPoint endPoint) public static LoopbackDnsServer Start() { - UdpClient udp; + Socket udp = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + if (OperatingSystem.IsWindows()) + { + // Disable ICMP "port unreachable" surfacing as WSAECONNRESET on ReceiveFrom + const int SIO_UDP_CONNRESET = -1744830452; + udp.IOControl(SIO_UDP_CONNRESET, new byte[] { 0 }, null); + } try { - udp = new UdpClient(new IPEndPoint(IPAddress.Loopback, DnsPort)); + udp.Bind(new IPEndPoint(IPAddress.Loopback, DnsPort)); } catch (SocketException ex) { + udp.Dispose(); throw new SkipTestException( $"Unable to bind loopback DNS port {DnsPort}; another DNS server may be running ({ex.SocketErrorCode})."); } - IPEndPoint ep = (IPEndPoint)udp.Client.LocalEndPoint!; - TcpListener tcp = new(IPAddress.Loopback, ep.Port); + IPEndPoint ep = (IPEndPoint)udp.LocalEndPoint!; + Socket tcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { - tcp.Start(); + tcp.Bind(new IPEndPoint(IPAddress.Loopback, ep.Port)); + tcp.Listen(); } catch (SocketException ex) { udp.Dispose(); + tcp.Dispose(); throw new SkipTestException( $"Unable to bind loopback DNS TCP port {DnsPort}; another DNS server may be running ({ex.SocketErrorCode})."); } @@ -82,6 +90,8 @@ public static LoopbackDnsServer Start() return new LoopbackDnsServer(udp, tcp, ep); } + public void ClearResponses() => _responses.Clear(); + public void AddResponse(string name, DnsRecordType type, Func configure) { _responses[(name.ToLowerInvariant(), type)] = (queryId, qName, _) => @@ -101,18 +111,29 @@ private async Task ListenUdpAsync(CancellationToken ct) { while (!ct.IsCancellationRequested) { - UdpReceiveResult result = await _udp.ReceiveAsync(ct); - Interlocked.Increment(ref _requestCount); - - byte[] response = ProcessQuery(result.Buffer); - if (response.Length > 0) + byte[] buffer = new byte[4096]; + SocketReceiveFromResult result = await _udp.ReceiveFromAsync( + buffer, SocketFlags.None, new IPEndPoint(IPAddress.Loopback, 0), ct); + byte[] query = buffer[..result.ReceivedBytes]; + EndPoint remote = result.RemoteEndPoint; + _ = Task.Run(async () => { - await _udp.SendAsync(response, result.RemoteEndPoint, ct); - } + Interlocked.Increment(ref _requestCount); + + byte[] response = ProcessQuery(query); + if (response.Length > 0) + { + await _udp.SendToAsync(response, SocketFlags.None, remote, ct); + } + }); } } catch (OperationCanceledException) { } catch (ObjectDisposedException) { } + catch (Exception ex) + { + Debug.Fail($"Unexpected exception in UDP listener: {ex}"); + } } private async Task ListenTcpAsync(CancellationToken ct) @@ -121,31 +142,33 @@ private async Task ListenTcpAsync(CancellationToken ct) { while (!ct.IsCancellationRequested) { - TcpClient client = await _tcp.AcceptTcpClientAsync(ct); - _ = HandleTcpClientAsync(client, ct); + Socket client = await _tcp.AcceptAsync(ct); + _ = Task.Run(() => HandleTcpClientAsync(client, ct)); } } catch (OperationCanceledException) { } catch (ObjectDisposedException) { } + catch (Exception ex) + { + Debug.Fail($"Unexpected exception in TCP listener: {ex}"); + } } - private async Task HandleTcpClientAsync(TcpClient client, CancellationToken ct) + private async Task HandleTcpClientAsync(Socket client, CancellationToken ct) { try { using (client) { - NetworkStream stream = client.GetStream(); - byte[] lengthBuf = new byte[2]; - if (!await ReadExactlyAsync(stream, lengthBuf, ct)) + if (!await ReadExactlyAsync(client, lengthBuf, ct)) { return; } int queryLength = BinaryPrimitives.ReadUInt16BigEndian(lengthBuf); byte[] query = new byte[queryLength]; - if (!await ReadExactlyAsync(stream, query, ct)) + if (!await ReadExactlyAsync(client, query, ct)) { return; } @@ -158,22 +181,22 @@ private async Task HandleTcpClientAsync(TcpClient client, CancellationToken ct) { byte[] responseLengthBuf = new byte[2]; BinaryPrimitives.WriteUInt16BigEndian(responseLengthBuf, (ushort)response.Length); - await stream.WriteAsync(responseLengthBuf, ct); - await stream.WriteAsync(response, ct); + await client.SendAsync(responseLengthBuf, SocketFlags.None, ct); + await client.SendAsync(response, SocketFlags.None, ct); } } } catch (OperationCanceledException) { } catch (ObjectDisposedException) { } - catch (IOException) { } + catch (SocketException) { } } - private static async Task ReadExactlyAsync(NetworkStream stream, byte[] buffer, CancellationToken ct) + private static async Task ReadExactlyAsync(Socket socket, byte[] buffer, CancellationToken ct) { int read = 0; while (read < buffer.Length) { - int n = await stream.ReadAsync(buffer.AsMemory(read), ct); + int n = await socket.ReceiveAsync(buffer.AsMemory(read), SocketFlags.None, ct); if (n == 0) { return false; @@ -266,7 +289,7 @@ public async ValueTask DisposeAsync() { _cts.Cancel(); _udp.Dispose(); - _tcp.Stop(); + _tcp.Dispose(); try { await _udpListenTask; } catch { } try { await _tcpListenTask; } catch { } _cts.Dispose(); From 73acd3c9db98e30483b9aaa9d101279999078545 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Thu, 11 Jun 2026 13:58:54 +0200 Subject: [PATCH 05/19] Add synchronous DnsResolver methods using synchronous DnsQueryEx 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> --- .../src/System/Net/Dns.Resolve.cs | 5 +- .../src/System/Net/DnsResolver.Unsupported.cs | 21 + .../src/System/Net/DnsResolver.Windows.cs | 372 ++++++++++++------ .../System/Net/DnsResolver.WindowsAsync.cs | 105 ++++- .../src/System/Net/DnsResolver.cs | 44 ++- .../DnsResolverLoopbackTest.cs | 166 +++++--- .../tests/FunctionalTests/DnsResolverTest.cs | 18 + 7 files changed, 528 insertions(+), 203 deletions(-) diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/Dns.Resolve.cs b/src/libraries/System.Net.NameResolution/src/System/Net/Dns.Resolve.cs index d96bf318a81f1d..eaa2b55eac439e 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/Dns.Resolve.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/Dns.Resolve.cs @@ -56,7 +56,10 @@ public static DnsResult ResolvePtr(string name) => DefaultResolver.ResolvePtr(name); public static DnsResult ResolvePtr(IPAddress address) - => DefaultResolver.ResolvePtrAsync(address).GetAwaiter().GetResult(); + { + ArgumentNullException.ThrowIfNull(address); + return DefaultResolver.ResolvePtr(DnsResolver.BuildArpaName(address)); + } public static Task> ResolvePtrAsync(string name, CancellationToken cancellationToken = default) => DefaultResolver.ResolvePtrAsync(name, cancellationToken); diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Unsupported.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Unsupported.cs index 0e3e4543ae763c..8cf6b276e0551b 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Unsupported.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Unsupported.cs @@ -31,5 +31,26 @@ private Task> ResolvePtrCoreAsync(string name, Cancellation private Task> ResolveNsCoreAsync(string name, CancellationToken cancellationToken) => throw new PlatformNotSupportedException(); + + private DnsResult ResolveAddressesCore(string name, AddressFamily addressFamily, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + + private DnsResult ResolveSrvCore(string name, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + + private DnsResult ResolveMxCore(string name, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + + private DnsResult ResolveTxtCore(string name, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + + private DnsResult ResolveCNameCore(string name, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + + private DnsResult ResolvePtrCore(string name, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + + private DnsResult ResolveNsCore(string name, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); } } diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Windows.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Windows.cs index f7b8727f39e370..b6e16b1eaec498 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Windows.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Windows.cs @@ -13,7 +13,7 @@ namespace System.Net { public sealed partial class DnsResolver { - // ---- Public Resolve*Core methods (called from cross-platform DnsResolver) ---- + // ---- Asynchronous Resolve*Core methods (called from cross-platform DnsResolver) ---- private async Task> ResolveAddressesCoreAsync(string name, AddressFamily addressFamily, CancellationToken cancellationToken) { @@ -27,12 +27,7 @@ private async Task> ResolveAddressesCoreAsync(string na return MergeAddressResults(aRes, aaaaRes); } - ushort qtype = addressFamily switch - { - AddressFamily.InterNetwork => Interop.Dnsapi.DNS_TYPE_A, - AddressFamily.InterNetworkV6 => Interop.Dnsapi.DNS_TYPE_AAAA, - _ => throw new ArgumentException(SR.net_invalid_ip_addr, nameof(addressFamily)), - }; + ushort qtype = AddressFamilyToQueryType(addressFamily); return await QueryAddressesAsync(name, qtype, cancellationToken).ConfigureAwait(false); } @@ -41,36 +36,7 @@ private async Task> ResolveSrvCoreAsync(string name, Cancel DnsQueryRawResult raw = await DnsQueryExAsync(name, Interop.Dnsapi.DNS_TYPE_SRV, cancellationToken).ConfigureAwait(false); try { - if (raw.ResponseCode != DnsResponseCode.NoError) - { - return new DnsResult(raw.ResponseCode, null, raw.NegativeCacheTtl); - } - - // Gather additional-section A/AAAA records by name so we can attach them. - Dictionary>? glue = null; - ParseAdditionalAddresses(raw.RecordsHead, ref glue); - - List records = new(); - for (IntPtr cur = raw.RecordsHead; cur != IntPtr.Zero; ) - { - Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); - uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; - if (hdr.wType == Interop.Dnsapi.DNS_TYPE_SRV && section == Interop.Dnsapi.DNSREC_ANSWER) - { - IntPtr dataPtr = cur + Marshal.SizeOf(); - Interop.Dnsapi.DNS_SRV_DATA data = Marshal.PtrToStructure(dataPtr); - string target = PtrToString(data.pNameTarget) ?? string.Empty; - IReadOnlyList? attached = null; - if (glue != null && glue.TryGetValue(target, out List? list)) - { - attached = list; - } - records.Add(new SrvRecord(target, data.wPort, data.wPriority, data.wWeight, TimeSpan.FromSeconds(hdr.dwTtl), attached)); - } - cur = hdr.pNext; - } - - return new DnsResult(DnsResponseCode.NoError, records, TimeSpan.Zero); + return ParseSrv(raw); } finally { @@ -79,73 +45,76 @@ private async Task> ResolveSrvCoreAsync(string name, Cancel } private Task> ResolveMxCoreAsync(string name, CancellationToken cancellationToken) - => QuerySimpleAsync(name, Interop.Dnsapi.DNS_TYPE_MX, cancellationToken, static (hdr, dataPtr) => - { - Interop.Dnsapi.DNS_MX_DATA data = Marshal.PtrToStructure(dataPtr); - return new MxRecord(PtrToString(data.pNameExchange) ?? string.Empty, data.wPreference, TimeSpan.FromSeconds(hdr.dwTtl)); - }); + => QuerySimpleAsync(name, Interop.Dnsapi.DNS_TYPE_MX, cancellationToken, s_parseMx); private Task> ResolveCNameCoreAsync(string name, CancellationToken cancellationToken) - => QuerySimpleAsync(name, Interop.Dnsapi.DNS_TYPE_CNAME, cancellationToken, static (hdr, dataPtr) => - { - Interop.Dnsapi.DNS_CNAME_DATA data = Marshal.PtrToStructure(dataPtr); - return new CNameRecord(PtrToString(data.pNameHost) ?? string.Empty, TimeSpan.FromSeconds(hdr.dwTtl)); - }); + => QuerySimpleAsync(name, Interop.Dnsapi.DNS_TYPE_CNAME, cancellationToken, s_parseCName); private Task> ResolvePtrCoreAsync(string name, CancellationToken cancellationToken) - => QuerySimpleAsync(name, Interop.Dnsapi.DNS_TYPE_PTR, cancellationToken, static (hdr, dataPtr) => - { - Interop.Dnsapi.DNS_PTR_DATA data = Marshal.PtrToStructure(dataPtr); - return new PtrRecord(PtrToString(data.pNameHost) ?? string.Empty, TimeSpan.FromSeconds(hdr.dwTtl)); - }); + => QuerySimpleAsync(name, Interop.Dnsapi.DNS_TYPE_PTR, cancellationToken, s_parsePtr); private Task> ResolveNsCoreAsync(string name, CancellationToken cancellationToken) - => QuerySimpleAsync(name, Interop.Dnsapi.DNS_TYPE_NS, cancellationToken, static (hdr, dataPtr) => - { - Interop.Dnsapi.DNS_NS_DATA data = Marshal.PtrToStructure(dataPtr); - return new NsRecord(PtrToString(data.pNameHost) ?? string.Empty, TimeSpan.FromSeconds(hdr.dwTtl)); - }); + => QuerySimpleAsync(name, Interop.Dnsapi.DNS_TYPE_NS, cancellationToken, s_parseNs); private async Task> ResolveTxtCoreAsync(string name, CancellationToken cancellationToken) { DnsQueryRawResult raw = await DnsQueryExAsync(name, Interop.Dnsapi.DNS_TYPE_TEXT, cancellationToken).ConfigureAwait(false); try { - if (raw.ResponseCode != DnsResponseCode.NoError) - { - return new DnsResult(raw.ResponseCode, null, raw.NegativeCacheTtl); - } + return ParseTxt(raw); + } + finally + { + raw.Dispose(); + } + } - List records = new(); - for (IntPtr cur = raw.RecordsHead; cur != IntPtr.Zero; ) - { - Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); - uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; - if (hdr.wType == Interop.Dnsapi.DNS_TYPE_TEXT && section == Interop.Dnsapi.DNSREC_ANSWER) - { - IntPtr dataPtr = cur + Marshal.SizeOf(); - // DNS_TXT_DATA: uint dwStringCount; followed by array of PCWSTR. - uint count = (uint)Marshal.ReadInt32(dataPtr); - int ptrSize = IntPtr.Size; - IntPtr stringsPtr = dataPtr + sizeof(uint); - if (ptrSize > sizeof(uint)) - { - // Round up to pointer alignment. - long aligned = ((long)stringsPtr + (ptrSize - 1)) & ~(long)(ptrSize - 1); - stringsPtr = checked((nint)aligned); - } - string[] values = new string[count]; - for (int i = 0; i < count; i++) - { - IntPtr strPtr = Marshal.ReadIntPtr(stringsPtr, i * ptrSize); - values[i] = PtrToString(strPtr) ?? string.Empty; - } - records.Add(new TxtRecord(values, TimeSpan.FromSeconds(hdr.dwTtl))); - } - cur = hdr.pNext; - } + // ---- Synchronous Resolve*Core methods (called from cross-platform DnsResolver) ---- - return new DnsResult(DnsResponseCode.NoError, records, TimeSpan.Zero); + private DnsResult ResolveAddressesCore(string name, AddressFamily addressFamily, CancellationToken cancellationToken) + { + if (addressFamily == AddressFamily.Unspecified) + { + DnsResult aRes = QueryAddresses(name, Interop.Dnsapi.DNS_TYPE_A, cancellationToken); + DnsResult aaaaRes = QueryAddresses(name, Interop.Dnsapi.DNS_TYPE_AAAA, cancellationToken); + return MergeAddressResults(aRes, aaaaRes); + } + + ushort qtype = AddressFamilyToQueryType(addressFamily); + return QueryAddresses(name, qtype, cancellationToken); + } + + private DnsResult ResolveSrvCore(string name, CancellationToken cancellationToken) + { + DnsQueryRawResult raw = DnsQueryExSync(name, Interop.Dnsapi.DNS_TYPE_SRV, cancellationToken); + try + { + return ParseSrv(raw); + } + finally + { + raw.Dispose(); + } + } + + private DnsResult ResolveMxCore(string name, CancellationToken cancellationToken) + => QuerySimple(name, Interop.Dnsapi.DNS_TYPE_MX, cancellationToken, s_parseMx); + + private DnsResult ResolveCNameCore(string name, CancellationToken cancellationToken) + => QuerySimple(name, Interop.Dnsapi.DNS_TYPE_CNAME, cancellationToken, s_parseCName); + + private DnsResult ResolvePtrCore(string name, CancellationToken cancellationToken) + => QuerySimple(name, Interop.Dnsapi.DNS_TYPE_PTR, cancellationToken, s_parsePtr); + + private DnsResult ResolveNsCore(string name, CancellationToken cancellationToken) + => QuerySimple(name, Interop.Dnsapi.DNS_TYPE_NS, cancellationToken, s_parseNs); + + private DnsResult ResolveTxtCore(string name, CancellationToken cancellationToken) + { + DnsQueryRawResult raw = DnsQueryExSync(name, Interop.Dnsapi.DNS_TYPE_TEXT, cancellationToken); + try + { + return ParseTxt(raw); } finally { @@ -153,40 +122,191 @@ private async Task> ResolveTxtCoreAsync(string name, Cancel } } - // ---- Helpers for address parsing ---- + // ---- Per-record-type selectors (shared by sync and async paths) ---- + + private static readonly Func s_parseMx = static (hdr, dataPtr) => + { + Interop.Dnsapi.DNS_MX_DATA data = Marshal.PtrToStructure(dataPtr); + return new MxRecord(PtrToString(data.pNameExchange) ?? string.Empty, data.wPreference, TimeSpan.FromSeconds(hdr.dwTtl)); + }; + + private static readonly Func s_parseCName = static (hdr, dataPtr) => + { + Interop.Dnsapi.DNS_CNAME_DATA data = Marshal.PtrToStructure(dataPtr); + return new CNameRecord(PtrToString(data.pNameHost) ?? string.Empty, TimeSpan.FromSeconds(hdr.dwTtl)); + }; + + private static readonly Func s_parsePtr = static (hdr, dataPtr) => + { + Interop.Dnsapi.DNS_PTR_DATA data = Marshal.PtrToStructure(dataPtr); + return new PtrRecord(PtrToString(data.pNameHost) ?? string.Empty, TimeSpan.FromSeconds(hdr.dwTtl)); + }; + + private static readonly Func s_parseNs = static (hdr, dataPtr) => + { + Interop.Dnsapi.DNS_NS_DATA data = Marshal.PtrToStructure(dataPtr); + return new NsRecord(PtrToString(data.pNameHost) ?? string.Empty, TimeSpan.FromSeconds(hdr.dwTtl)); + }; + + private static ushort AddressFamilyToQueryType(AddressFamily addressFamily) => + addressFamily switch + { + AddressFamily.InterNetwork => Interop.Dnsapi.DNS_TYPE_A, + AddressFamily.InterNetworkV6 => Interop.Dnsapi.DNS_TYPE_AAAA, + _ => throw new ArgumentException(SR.net_invalid_ip_addr, nameof(addressFamily)), + }; + + // ---- Address query helpers ---- private async Task> QueryAddressesAsync(string name, ushort qtype, CancellationToken cancellationToken) { DnsQueryRawResult raw = await DnsQueryExAsync(name, qtype, cancellationToken).ConfigureAwait(false); try { - if (raw.ResponseCode != DnsResponseCode.NoError) + return ParseAddresses(raw, qtype); + } + finally + { + raw.Dispose(); + } + } + + private DnsResult QueryAddresses(string name, ushort qtype, CancellationToken cancellationToken) + { + DnsQueryRawResult raw = DnsQueryExSync(name, qtype, cancellationToken); + try + { + return ParseAddresses(raw, qtype); + } + finally + { + raw.Dispose(); + } + } + + // ---- Record-list parsers (sync/async agnostic) ---- + + private static DnsResult ParseAddresses(DnsQueryRawResult raw, ushort qtype) + { + if (raw.ResponseCode != DnsResponseCode.NoError) + { + return new DnsResult(raw.ResponseCode, null, raw.NegativeCacheTtl); + } + + List records = new(); + for (IntPtr cur = raw.RecordsHead; cur != IntPtr.Zero; ) + { + Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); + uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; + if (hdr.wType == qtype && section == Interop.Dnsapi.DNSREC_ANSWER) { - return new DnsResult(raw.ResponseCode, null, raw.NegativeCacheTtl); + IntPtr dataPtr = cur + Marshal.SizeOf(); + if (TryParseAddress(hdr.wType, dataPtr, out IPAddress? address)) + { + records.Add(new AddressRecord(address!, TimeSpan.FromSeconds(hdr.dwTtl))); + } } + cur = hdr.pNext; + } + + return new DnsResult(DnsResponseCode.NoError, records, TimeSpan.Zero); + } - List records = new(); - for (IntPtr cur = raw.RecordsHead; cur != IntPtr.Zero; ) + private static DnsResult ParseSrv(DnsQueryRawResult raw) + { + if (raw.ResponseCode != DnsResponseCode.NoError) + { + return new DnsResult(raw.ResponseCode, null, raw.NegativeCacheTtl); + } + + // Gather additional-section A/AAAA records by name so we can attach them. + Dictionary>? glue = null; + ParseAdditionalAddresses(raw.RecordsHead, ref glue); + + List records = new(); + for (IntPtr cur = raw.RecordsHead; cur != IntPtr.Zero; ) + { + Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); + uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; + if (hdr.wType == Interop.Dnsapi.DNS_TYPE_SRV && section == Interop.Dnsapi.DNSREC_ANSWER) { - Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); - uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; - if (hdr.wType == qtype && section == Interop.Dnsapi.DNSREC_ANSWER) + IntPtr dataPtr = cur + Marshal.SizeOf(); + Interop.Dnsapi.DNS_SRV_DATA data = Marshal.PtrToStructure(dataPtr); + string target = PtrToString(data.pNameTarget) ?? string.Empty; + IReadOnlyList? attached = null; + if (glue != null && glue.TryGetValue(target, out List? list)) { - IntPtr dataPtr = cur + Marshal.SizeOf(); - if (TryParseAddress(hdr.wType, dataPtr, out IPAddress? address)) - { - records.Add(new AddressRecord(address!, TimeSpan.FromSeconds(hdr.dwTtl))); - } + attached = list; } - cur = hdr.pNext; + records.Add(new SrvRecord(target, data.wPort, data.wPriority, data.wWeight, TimeSpan.FromSeconds(hdr.dwTtl), attached)); } + cur = hdr.pNext; + } - return new DnsResult(DnsResponseCode.NoError, records, TimeSpan.Zero); + return new DnsResult(DnsResponseCode.NoError, records, TimeSpan.Zero); + } + + private static DnsResult ParseTxt(DnsQueryRawResult raw) + { + if (raw.ResponseCode != DnsResponseCode.NoError) + { + return new DnsResult(raw.ResponseCode, null, raw.NegativeCacheTtl); } - finally + + List records = new(); + for (IntPtr cur = raw.RecordsHead; cur != IntPtr.Zero; ) { - raw.Dispose(); + Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); + uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; + if (hdr.wType == Interop.Dnsapi.DNS_TYPE_TEXT && section == Interop.Dnsapi.DNSREC_ANSWER) + { + IntPtr dataPtr = cur + Marshal.SizeOf(); + // DNS_TXT_DATA: uint dwStringCount; followed by array of PCWSTR. + uint count = (uint)Marshal.ReadInt32(dataPtr); + int ptrSize = IntPtr.Size; + IntPtr stringsPtr = dataPtr + sizeof(uint); + if (ptrSize > sizeof(uint)) + { + // Round up to pointer alignment. + long aligned = ((long)stringsPtr + (ptrSize - 1)) & ~(long)(ptrSize - 1); + stringsPtr = checked((nint)aligned); + } + string[] values = new string[count]; + for (int i = 0; i < count; i++) + { + IntPtr strPtr = Marshal.ReadIntPtr(stringsPtr, i * ptrSize); + values[i] = PtrToString(strPtr) ?? string.Empty; + } + records.Add(new TxtRecord(values, TimeSpan.FromSeconds(hdr.dwTtl))); + } + cur = hdr.pNext; } + + return new DnsResult(DnsResponseCode.NoError, records, TimeSpan.Zero); + } + + private static DnsResult ParseSimple(DnsQueryRawResult raw, ushort qtype, + Func selector) + { + if (raw.ResponseCode != DnsResponseCode.NoError) + { + return new DnsResult(raw.ResponseCode, null, raw.NegativeCacheTtl); + } + + List records = new(); + for (IntPtr cur = raw.RecordsHead; cur != IntPtr.Zero; ) + { + Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); + uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; + if (hdr.wType == qtype && section == Interop.Dnsapi.DNSREC_ANSWER) + { + IntPtr dataPtr = cur + Marshal.SizeOf(); + records.Add(selector(hdr, dataPtr)); + } + cur = hdr.pNext; + } + + return new DnsResult(DnsResponseCode.NoError, records, TimeSpan.Zero); } private static DnsResult MergeAddressResults(DnsResult a, DnsResult b) @@ -252,7 +372,7 @@ private static void ParseAdditionalAddresses(IntPtr head, ref Dictionary> QuerySimpleAsync(string name, ushort qtype, CancellationToken cancellationToken, Func selector) @@ -260,23 +380,21 @@ private async Task> QuerySimpleAsync(string name, us DnsQueryRawResult raw = await DnsQueryExAsync(name, qtype, cancellationToken).ConfigureAwait(false); try { - if (raw.ResponseCode != DnsResponseCode.NoError) - { - return new DnsResult(raw.ResponseCode, null, raw.NegativeCacheTtl); - } - List records = new(); - for (IntPtr cur = raw.RecordsHead; cur != IntPtr.Zero; ) - { - Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); - uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; - if (hdr.wType == qtype && section == Interop.Dnsapi.DNSREC_ANSWER) - { - IntPtr dataPtr = cur + Marshal.SizeOf(); - records.Add(selector(hdr, dataPtr)); - } - cur = hdr.pNext; - } - return new DnsResult(DnsResponseCode.NoError, records, TimeSpan.Zero); + return ParseSimple(raw, qtype, selector); + } + finally + { + raw.Dispose(); + } + } + + private DnsResult QuerySimple(string name, ushort qtype, CancellationToken cancellationToken, + Func selector) + { + DnsQueryRawResult raw = DnsQueryExSync(name, qtype, cancellationToken); + try + { + return ParseSimple(raw, qtype, selector); } finally { @@ -284,7 +402,7 @@ private async Task> QuerySimpleAsync(string name, us } } - // ---- Core DnsQueryEx async wrapper ---- + // ---- Core DnsQueryEx wrappers ---- private unsafe Task DnsQueryExAsync(string name, ushort queryType, CancellationToken cancellationToken) { @@ -300,7 +418,7 @@ private unsafe Task DnsQueryExAsync(string name, ushort query private static unsafe string? PtrToString(IntPtr p) => p == IntPtr.Zero ? null : Marshal.PtrToStringUni(p); - // ---- Raw query result returned by the low-level helper ---- + // ---- Raw query result returned by the low-level helpers ---- private readonly struct DnsQueryRawResult : IDisposable { diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.WindowsAsync.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.WindowsAsync.cs index 86e68a2f26c6ad..326e14a783754b 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.WindowsAsync.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.WindowsAsync.cs @@ -50,21 +50,7 @@ public DnsQueryAsyncState(IList servers, string name, ushort queryTy public Task StartAsync() { - // DnsQueryEx only supports DNS servers reachable on the standard port 53. - // The sockaddr port field passed to the API must be 0 (the API always - // queries port 53); supplying any non-zero port - even 53 itself - results - // in ERROR_INVALID_PARAMETER. We therefore reject any server endpoint that - // requests a non-default port, since it cannot be honored on Windows. - if (_servers is { Count: > 0 }) - { - foreach (IPEndPoint ep in _servers) - { - if (ep.Port != 0 && ep.Port != 53) - { - throw new PlatformNotSupportedException(SR.net_dns_custom_port_not_supported); - } - } - } + ValidateServerPorts(_servers); try { @@ -248,6 +234,95 @@ private static void QueryCompletionCallback(IntPtr pQueryContext, IntPtr pQueryR } } + // DnsQueryEx only supports DNS servers reachable on the standard port 53. + // The sockaddr port field passed to the API must be 0 (the API always + // queries port 53); supplying any non-zero port - even 53 itself - results + // in ERROR_INVALID_PARAMETER. We therefore reject any server endpoint that + // requests a non-default port, since it cannot be honored on Windows. + private static void ValidateServerPorts(IList servers) + { + if (servers is { Count: > 0 }) + { + foreach (IPEndPoint ep in servers) + { + if (ep.Port != 0 && ep.Port != 53) + { + throw new PlatformNotSupportedException(SR.net_dns_custom_port_not_supported); + } + } + } + } + + // Synchronous DnsQueryEx invocation. By omitting the completion callback the + // API executes the query inline on the calling thread and returns the result + // directly, so no GCHandle / TaskCompletionSource bookkeeping is required. + private unsafe DnsQueryRawResult DnsQueryExSync(string name, ushort queryType, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + ValidateServerPorts(_options.Servers); + + IntPtr namePtr = IntPtr.Zero; + IntPtr serverListPtr = IntPtr.Zero; + try + { + namePtr = Marshal.StringToHGlobalUni(name); + + Interop.Dnsapi.DNS_QUERY_RESULT result = default; + result.Version = Interop.Dnsapi.DNS_QUERY_REQUEST_VERSION1; + + Interop.Dnsapi.DNS_QUERY_REQUEST request = default; + request.Version = Interop.Dnsapi.DNS_QUERY_REQUEST_VERSION1; + request.QueryName = namePtr; + request.QueryType = queryType; + request.QueryOptions = Interop.Dnsapi.DNS_QUERY_STANDARD; + // No completion callback => synchronous execution. + + if (_options.Servers is { Count: > 0 }) + { + BuildAddrArray(_options.Servers, out serverListPtr); + request.pDnsServerList = (Interop.Dnsapi.DNS_ADDR_ARRAY*)serverListPtr; + } + + // A null cancel handle is valid for synchronous queries. + int status = Interop.Dnsapi.DnsQueryEx(&request, &result, null); + + IntPtr records = result.pQueryRecords; + + if (cancellationToken.IsCancellationRequested) + { + if (records != IntPtr.Zero) + { + Interop.Dnsapi.DnsFree(records, Interop.Dnsapi.DnsFreeRecordList); + } + throw new OperationCanceledException(cancellationToken); + } + + DnsResponseCode rc = MapWindowsErrorToResponseCode(status); + + // 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); + } + + return new DnsQueryRawResult(rc, records, negativeTtl); + } + finally + { + if (namePtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(namePtr); + } + if (serverListPtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(serverListPtr); + } + } + } + private static unsafe void BuildAddrArray(IList servers, out IntPtr arrayPtr) { int count = servers.Count; diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs index 01e78c216e08cc..f47a53ebfd5123 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs @@ -28,28 +28,56 @@ public DnsResolver(DnsResolverOptions options) } public DnsResult ResolveAddresses(string name) - => ResolveAddressesAsync(name).GetAwaiter().GetResult(); + => ResolveAddresses(name, AddressFamily.Unspecified); public DnsResult ResolveAddresses(string name, AddressFamily addressFamily) - => ResolveAddressesAsync(name, addressFamily).GetAwaiter().GetResult(); + { + ValidateName(name); + ObjectDisposedException.ThrowIf(_disposed, this); + return ResolveAddressesCore(name, addressFamily, default); + } public DnsResult ResolveSrv(string name) - => ResolveSrvAsync(name).GetAwaiter().GetResult(); + { + ValidateName(name); + ObjectDisposedException.ThrowIf(_disposed, this); + return ResolveSrvCore(name, default); + } public DnsResult ResolveMx(string name) - => ResolveMxAsync(name).GetAwaiter().GetResult(); + { + ValidateName(name); + ObjectDisposedException.ThrowIf(_disposed, this); + return ResolveMxCore(name, default); + } public DnsResult ResolveTxt(string name) - => ResolveTxtAsync(name).GetAwaiter().GetResult(); + { + ValidateName(name); + ObjectDisposedException.ThrowIf(_disposed, this); + return ResolveTxtCore(name, default); + } public DnsResult ResolveCName(string name) - => ResolveCNameAsync(name).GetAwaiter().GetResult(); + { + ValidateName(name); + ObjectDisposedException.ThrowIf(_disposed, this); + return ResolveCNameCore(name, default); + } public DnsResult ResolvePtr(string name) - => ResolvePtrAsync(name).GetAwaiter().GetResult(); + { + ValidateName(name); + ObjectDisposedException.ThrowIf(_disposed, this); + return ResolvePtrCore(name, default); + } public DnsResult ResolveNs(string name) - => ResolveNsAsync(name).GetAwaiter().GetResult(); + { + ValidateName(name); + ObjectDisposedException.ThrowIf(_disposed, this); + return ResolveNsCore(name, default); + } public Task> ResolveAddressesAsync(string name, CancellationToken cancellationToken = default) => ResolveAddressesAsync(name, AddressFamily.Unspecified, cancellationToken); diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs index 0f240ff92777db..353df8fc6f167a 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs @@ -37,6 +37,9 @@ public async ValueTask DisposeAsync() // tests are skipped via SkipTestException rather than failing. Because the single // machine-wide port 53 is shared, these tests run sequentially (see the collection). // + // Each behavioral test is parameterized over the synchronous and asynchronous APIs + // so both code paths are exercised against the same loopback responses. + // // These tests cover the record-parsing and response-handling behavior that the // OuterLoop tests in DnsResolverTest.cs cannot exercise deterministically. [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] @@ -62,16 +65,43 @@ public DnsResolverLoopbackTest(WindowsLoopbackServer fixture) internal DnsResolver Resolver => _resolver ??= CreateResolver(_server); + // ---- Sync/async dispatch helpers ---- + // The synchronous overloads execute inline on the calling thread; the results + // are wrapped in a completed Task so each test can await a single helper. + + private static async Task> ResolveAddresses(bool async, DnsResolver resolver, string name, AddressFamily addressFamily = AddressFamily.Unspecified) + => async ? await resolver.ResolveAddressesAsync(name, addressFamily) : resolver.ResolveAddresses(name, addressFamily); + + private static async Task> ResolveSrv(bool async, DnsResolver resolver, string name) + => async ? await resolver.ResolveSrvAsync(name) : resolver.ResolveSrv(name); + + private static async Task> ResolveMx(bool async, DnsResolver resolver, string name) + => async ? await resolver.ResolveMxAsync(name) : resolver.ResolveMx(name); + + private static async Task> ResolveTxt(bool async, DnsResolver resolver, string name) + => async ? await resolver.ResolveTxtAsync(name) : resolver.ResolveTxt(name); + + private static async Task> ResolveCName(bool async, DnsResolver resolver, string name) + => async ? await resolver.ResolveCNameAsync(name) : resolver.ResolveCName(name); + + private static async Task> ResolvePtr(bool async, DnsResolver resolver, string name) + => async ? await resolver.ResolvePtrAsync(name) : resolver.ResolvePtr(name); + + private static async Task> ResolveNs(bool async, DnsResolver resolver, string name) + => async ? await resolver.ResolveNsAsync(name) : resolver.ResolveNs(name); + // ---- Address resolution ---- - [Fact] - public async Task ResolveAddresses_Unspecified_ReturnsBothV4AndV6() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveAddresses_Unspecified_ReturnsBothV4AndV6(bool async) { string name = UniqueName("host"); _server.AddResponse(name, DnsRecordType.A, b => b.Answer(new byte[] { 10, 0, 0, 1 }, ttl: 120)); _server.AddResponse(name, DnsRecordType.AAAA, b => b.Answer(IPAddress.Parse("fd00::1").GetAddressBytes(), ttl: 60)); - DnsResult result = await Resolver.ResolveAddressesAsync(name); + DnsResult result = await ResolveAddresses(async, Resolver, name); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); Assert.Equal(2, result.Records.Count); @@ -79,14 +109,16 @@ public async Task ResolveAddresses_Unspecified_ReturnsBothV4AndV6() Assert.Contains(result.Records, a => a.Address.ToString() == "fd00::1"); } - [Fact] - public async Task ResolveAddresses_IPv4Only_ReturnsOnlyV4() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveAddresses_IPv4Only_ReturnsOnlyV4(bool async) { string name = UniqueName("v4"); _server.AddResponse(name, DnsRecordType.A, b => b.Answer(new byte[] { 10, 0, 0, 2 }, ttl: 300)); _server.AddResponse(name, DnsRecordType.AAAA, b => b.ResponseCode(DnsResponseCode.NxDomain)); - DnsResult result = await Resolver.ResolveAddressesAsync(name); + DnsResult result = await ResolveAddresses(async, Resolver, name); // A succeeds, AAAA returns NXDOMAIN — overall is success because we got addresses. Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); @@ -94,41 +126,47 @@ public async Task ResolveAddresses_IPv4Only_ReturnsOnlyV4() Assert.Equal("10.0.0.2", record.Address.ToString()); } - [Fact] - public async Task ResolveAddresses_IPv6Only_ReturnsOnlyV6() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveAddresses_IPv6Only_ReturnsOnlyV6(bool async) { string name = UniqueName("v6"); _server.AddResponse(name, DnsRecordType.A, b => b.ResponseCode(DnsResponseCode.NxDomain)); _server.AddResponse(name, DnsRecordType.AAAA, b => b.Answer(IPAddress.Parse("fd00::1").GetAddressBytes(), ttl: 60)); - DnsResult result = await Resolver.ResolveAddressesAsync(name); + DnsResult result = await ResolveAddresses(async, Resolver, name); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); AddressRecord record = Assert.Single(result.Records); Assert.Equal("fd00::1", record.Address.ToString()); } - [Fact] - public async Task ResolveAddresses_AddressFamilyV4_QueriesOnlyA() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveAddresses_AddressFamilyV4_QueriesOnlyA(bool async) { string name = UniqueName("famv4"); _server.AddResponse(name, DnsRecordType.A, b => b.Answer(new byte[] { 192, 0, 2, 7 }, ttl: 200)); - DnsResult result = await Resolver.ResolveAddressesAsync(name, AddressFamily.InterNetwork); + DnsResult result = await ResolveAddresses(async, Resolver, name, AddressFamily.InterNetwork); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); AddressRecord record = Assert.Single(result.Records); Assert.Equal("192.0.2.7", record.Address.ToString()); } - [Fact] - public async Task ResolveAddresses_HasTtl() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveAddresses_HasTtl(bool async) { string name = UniqueName("ttl"); _server.AddResponse(name, DnsRecordType.A, b => b.Answer(new byte[] { 10, 0, 0, 1 }, ttl: 120)); _server.AddResponse(name, DnsRecordType.AAAA, b => b.ResponseCode(DnsResponseCode.NxDomain)); - DnsResult result = await Resolver.ResolveAddressesAsync(name); + DnsResult result = await ResolveAddresses(async, Resolver, name); AddressRecord record = Assert.Single(result.Records); // The TTL we sent (120s) should be preserved (custom-server queries bypass the OS cache). @@ -136,8 +174,10 @@ public async Task ResolveAddresses_HasTtl() $"Unexpected TTL: {record.Ttl}"); } - [Fact] - public async Task ResolveAddresses_Nxdomain_ReturnsNxDomain() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveAddresses_Nxdomain_ReturnsNxDomain(bool async) { string name = UniqueName("missing"); byte[] soaRdata = DnsResponseBuilder.BuildSoaRdata("test", 120); @@ -148,14 +188,16 @@ public async Task ResolveAddresses_Nxdomain_ReturnsNxDomain() .ResponseCode(DnsResponseCode.NxDomain) .Authority("test", DnsRecordType.SOA, soaRdata, ttl: 120)); - DnsResult result = await Resolver.ResolveAddressesAsync(name); + DnsResult result = await ResolveAddresses(async, Resolver, name); Assert.Equal(DnsResponseCode.NxDomain, result.ResponseCode); Assert.Empty(result.Records); } - [Fact] - public async Task ResolveAddresses_NoData_ReturnsNoErrorWithEmptyRecords() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveAddresses_NoData_ReturnsNoErrorWithEmptyRecords(bool async) { string name = UniqueName("nodata"); byte[] soaRdata = DnsResponseBuilder.BuildSoaRdata("test", 30); @@ -165,14 +207,16 @@ public async Task ResolveAddresses_NoData_ReturnsNoErrorWithEmptyRecords() .Authority("test", DnsRecordType.SOA, soaRdata, ttl: 30)); // The name exists but has no A/AAAA records → NODATA for both queries. - DnsResult result = await Resolver.ResolveAddressesAsync(name); + DnsResult result = await ResolveAddresses(async, Resolver, name); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); Assert.Empty(result.Records); } - [Fact] - public async Task ResolveAddresses_NoData_And_Nxdomain_AreDistinguishable() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveAddresses_NoData_And_Nxdomain_AreDistinguishable(bool async) { string nodataName = UniqueName("nodata"); byte[] soaRdata = DnsResponseBuilder.BuildSoaRdata("test", 30); @@ -190,11 +234,11 @@ public async Task ResolveAddresses_NoData_And_Nxdomain_AreDistinguishable() .ResponseCode(DnsResponseCode.NxDomain) .Authority("test", DnsRecordType.SOA, nxSoaRdata, ttl: 120)); - DnsResult nodata = await Resolver.ResolveAddressesAsync(nodataName); + DnsResult nodata = await ResolveAddresses(async, Resolver, nodataName); Assert.Equal(DnsResponseCode.NoError, nodata.ResponseCode); Assert.Empty(nodata.Records); - DnsResult nxdomain = await Resolver.ResolveAddressesAsync(missingName); + DnsResult nxdomain = await ResolveAddresses(async, Resolver, missingName); Assert.Equal(DnsResponseCode.NxDomain, nxdomain.ResponseCode); Assert.Empty(nxdomain.Records); @@ -203,15 +247,17 @@ public async Task ResolveAddresses_NoData_And_Nxdomain_AreDistinguishable() // ---- SRV ---- - [Fact] - public async Task ResolveSrv_ReturnsRecords() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveSrv_ReturnsRecords(bool async) { string name = $"_http._tcp.{UniqueName("svc")}"; _server.AddResponse(name, DnsRecordType.SRV, b => b .Answer(DnsResponseBuilder.BuildSrvRdata(10, 100, 8080, "node1.test"), ttl: 120) .Answer(DnsResponseBuilder.BuildSrvRdata(20, 50, 8081, "node2.test"), ttl: 120)); - DnsResult result = await Resolver.ResolveSrvAsync(name); + DnsResult result = await ResolveSrv(async, Resolver, name); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); Assert.Equal(2, result.Records.Count); @@ -226,8 +272,10 @@ public async Task ResolveSrv_ReturnsRecords() Assert.Equal((ushort)20, s2.Priority); } - [Fact] - public async Task ResolveSrv_IncludesAdditionalAddresses() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveSrv_IncludesAdditionalAddresses(bool async) { string name = $"_http._tcp.{UniqueName("svc")}"; _server.AddResponse(name, DnsRecordType.SRV, b => b @@ -237,7 +285,7 @@ public async Task ResolveSrv_IncludesAdditionalAddresses() .Additional("node2.test", DnsRecordType.A, new byte[] { 10, 0, 0, 11 }, ttl: 120) .Additional("node2.test", DnsRecordType.AAAA, IPAddress.Parse("fd00::11").GetAddressBytes(), ttl: 120)); - DnsResult result = await Resolver.ResolveSrvAsync(name); + DnsResult result = await ResolveSrv(async, Resolver, name); SrvRecord s1 = Assert.Single(result.Records, s => s.Target == "node1.test"); Assert.NotNull(s1.Addresses); @@ -249,14 +297,16 @@ public async Task ResolveSrv_IncludesAdditionalAddresses() Assert.Equal(2, s2.Addresses.Count); } - [Fact] - public async Task ResolveSrv_NoAdditionalAddresses() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveSrv_NoAdditionalAddresses(bool async) { string name = $"_noadd._tcp.{UniqueName("svc")}"; _server.AddResponse(name, DnsRecordType.SRV, b => b .Answer(DnsResponseBuilder.BuildSrvRdata(10, 100, 9090, "noaddr.test"), ttl: 60)); - DnsResult result = await Resolver.ResolveSrvAsync(name); + DnsResult result = await ResolveSrv(async, Resolver, name); SrvRecord record = Assert.Single(result.Records); Assert.Equal("noaddr.test", record.Target); @@ -265,15 +315,17 @@ public async Task ResolveSrv_NoAdditionalAddresses() // ---- MX / TXT / CNAME / PTR / NS ---- - [Fact] - public async Task ResolveMx_ReturnsRecords() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveMx_ReturnsRecords(bool async) { string name = UniqueName("mx"); _server.AddResponse(name, DnsRecordType.MX, b => b .Answer(DnsResponseBuilder.BuildMxRdata(10, "mail1.test"), ttl: 120) .Answer(DnsResponseBuilder.BuildMxRdata(20, "mail2.test"), ttl: 120)); - DnsResult result = await Resolver.ResolveMxAsync(name); + DnsResult result = await ResolveMx(async, Resolver, name); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); Assert.Equal(2, result.Records.Count); @@ -283,15 +335,17 @@ public async Task ResolveMx_ReturnsRecords() Assert.Single(result.Records, m => m.Exchange == "mail2.test" && m.Preference == 20); } - [Fact] - public async Task ResolveTxt_ReturnsValues() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveTxt_ReturnsValues(bool async) { string name = UniqueName("txt"); _server.AddResponse(name, DnsRecordType.TXT, b => b .Answer(DnsResponseBuilder.BuildTxtRdata("v=spf1 -all"), ttl: 120) .Answer(DnsResponseBuilder.BuildTxtRdata("part1", "part2"), ttl: 120)); - DnsResult result = await Resolver.ResolveTxtAsync(name); + DnsResult result = await ResolveTxt(async, Resolver, name); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); Assert.Equal(2, result.Records.Count); @@ -299,43 +353,49 @@ public async Task ResolveTxt_ReturnsValues() Assert.Contains(result.Records, t => t.Values.Count == 2 && t.Values[0] == "part1" && t.Values[1] == "part2"); } - [Fact] - public async Task ResolveCName_ReturnsCanonicalName() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveCName_ReturnsCanonicalName(bool async) { string name = UniqueName("alias"); _server.AddResponse(name, DnsRecordType.CNAME, b => b .Answer(DnsResponseBuilder.EncodeName("canonical.test"), ttl: 120)); - DnsResult result = await Resolver.ResolveCNameAsync(name); + DnsResult result = await ResolveCName(async, Resolver, name); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); CNameRecord record = Assert.Single(result.Records); Assert.Equal("canonical.test", record.CanonicalName); } - [Fact] - public async Task ResolvePtr_ReturnsName() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ResolvePtr_ReturnsName(bool async) { string name = $"1.0.0.10.in-addr.{UniqueName("arpa")}"; _server.AddResponse(name, DnsRecordType.PTR, b => b .Answer(DnsResponseBuilder.EncodeName("host.test"), ttl: 120)); - DnsResult result = await Resolver.ResolvePtrAsync(name); + DnsResult result = await ResolvePtr(async, Resolver, name); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); PtrRecord record = Assert.Single(result.Records); Assert.Equal("host.test", record.Name); } - [Fact] - public async Task ResolveNs_ReturnsRecords() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveNs_ReturnsRecords(bool async) { string name = UniqueName("ns"); _server.AddResponse(name, DnsRecordType.NS, b => b .Answer(DnsResponseBuilder.EncodeName("ns1.test"), ttl: 120) .Answer(DnsResponseBuilder.EncodeName("ns2.test"), ttl: 120)); - DnsResult result = await Resolver.ResolveNsAsync(name); + DnsResult result = await ResolveNs(async, Resolver, name); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); Assert.Equal(2, result.Records.Count); @@ -345,8 +405,10 @@ public async Task ResolveNs_ReturnsRecords() // ---- Custom server endpoint handling ---- - [Fact] - public async Task CustomServer_DefaultPortZero_IsAccepted() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task CustomServer_DefaultPortZero_IsAccepted(bool async) { // Port 0 means "use the default DNS port"; DnsQueryEx always queries port 53. using DnsResolver resolver = new DnsResolver(new DnsResolverOptions @@ -358,7 +420,7 @@ public async Task CustomServer_DefaultPortZero_IsAccepted() _server.AddResponse(name, DnsRecordType.A, b => b.Answer(new byte[] { 10, 0, 0, 5 }, ttl: 120)); _server.AddResponse(name, DnsRecordType.AAAA, b => b.ResponseCode(DnsResponseCode.NxDomain)); - DnsResult result = await resolver.ResolveAddressesAsync(name); + DnsResult result = await ResolveAddresses(async, resolver, name); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); AddressRecord record = Assert.Single(result.Records); diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs index 32e6bdf1aea582..be1f04eacb88a0 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs @@ -50,11 +50,25 @@ public async Task DnsResolver_NullName_Throws() await Assert.ThrowsAsync(() => r.ResolveNsAsync(null!)); } + [Fact] + public void DnsResolver_NullName_Throws_Sync() + { + using DnsResolver r = new DnsResolver(); + Assert.Throws(() => r.ResolveAddresses(null!)); + Assert.Throws(() => r.ResolveSrv(null!)); + Assert.Throws(() => r.ResolveMx(null!)); + Assert.Throws(() => r.ResolveTxt(null!)); + Assert.Throws(() => r.ResolveCName(null!)); + Assert.Throws(() => r.ResolvePtr((string)null!)); + Assert.Throws(() => r.ResolveNs(null!)); + } + [Fact] public async Task DnsResolver_EmptyName_Throws() { using DnsResolver r = new DnsResolver(); await Assert.ThrowsAsync(() => r.ResolveAddressesAsync(string.Empty)); + Assert.Throws(() => r.ResolveAddresses(string.Empty)); } [Fact] @@ -65,6 +79,9 @@ public async Task DnsResolver_Disposed_Throws() await Assert.ThrowsAsync(() => r.ResolveAddressesAsync(TestHost)); await Assert.ThrowsAsync(() => r.ResolveSrvAsync(TestSrv)); await Assert.ThrowsAsync(() => r.ResolveMxAsync(TestMxHost)); + Assert.Throws(() => r.ResolveAddresses(TestHost)); + Assert.Throws(() => r.ResolveSrv(TestSrv)); + Assert.Throws(() => r.ResolveMx(TestMxHost)); } [Fact] @@ -228,6 +245,7 @@ public async Task DnsResolver_CustomServer_NonStandardPort_ThrowsPlatformNotSupp }; using DnsResolver r = new DnsResolver(opts); await Assert.ThrowsAsync(() => r.ResolveAddressesAsync(TestHost)); + Assert.Throws(() => r.ResolveAddresses(TestHost)); } // ---- Reverse-arpa name building (covers both IPv4 and IPv6 paths used by ResolvePtr(IPAddress)) ---- From c375219421aafe39f6a0e74e4f9fc54ea728a720 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Thu, 11 Jun 2026 14:15:26 +0200 Subject: [PATCH 06/19] Unify sync/async DnsResolver core methods into single bool-async methods Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/Net/DnsResolver.Unsupported.cs | 35 +--- .../src/System/Net/DnsResolver.Windows.cs | 173 ++++++------------ .../src/System/Net/DnsResolver.cs | 30 +-- 3 files changed, 79 insertions(+), 159 deletions(-) diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Unsupported.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Unsupported.cs index 8cf6b276e0551b..5c7ff5d963634f 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Unsupported.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Unsupported.cs @@ -11,46 +11,25 @@ namespace System.Net { public sealed partial class DnsResolver { - private Task> ResolveAddressesCoreAsync(string name, AddressFamily addressFamily, CancellationToken cancellationToken) + private Task> ResolveAddressesCore(bool async, string name, AddressFamily addressFamily, CancellationToken cancellationToken) => throw new PlatformNotSupportedException(); - private Task> ResolveSrvCoreAsync(string name, CancellationToken cancellationToken) + private Task> ResolveSrvCore(bool async, string name, CancellationToken cancellationToken) => throw new PlatformNotSupportedException(); - private Task> ResolveMxCoreAsync(string name, CancellationToken cancellationToken) + private Task> ResolveMxCore(bool async, string name, CancellationToken cancellationToken) => throw new PlatformNotSupportedException(); - private Task> ResolveTxtCoreAsync(string name, CancellationToken cancellationToken) + private Task> ResolveTxtCore(bool async, string name, CancellationToken cancellationToken) => throw new PlatformNotSupportedException(); - private Task> ResolveCNameCoreAsync(string name, CancellationToken cancellationToken) + private Task> ResolveCNameCore(bool async, string name, CancellationToken cancellationToken) => throw new PlatformNotSupportedException(); - private Task> ResolvePtrCoreAsync(string name, CancellationToken cancellationToken) + private Task> ResolvePtrCore(bool async, string name, CancellationToken cancellationToken) => throw new PlatformNotSupportedException(); - private Task> ResolveNsCoreAsync(string name, CancellationToken cancellationToken) - => throw new PlatformNotSupportedException(); - - private DnsResult ResolveAddressesCore(string name, AddressFamily addressFamily, CancellationToken cancellationToken) - => throw new PlatformNotSupportedException(); - - private DnsResult ResolveSrvCore(string name, CancellationToken cancellationToken) - => throw new PlatformNotSupportedException(); - - private DnsResult ResolveMxCore(string name, CancellationToken cancellationToken) - => throw new PlatformNotSupportedException(); - - private DnsResult ResolveTxtCore(string name, CancellationToken cancellationToken) - => throw new PlatformNotSupportedException(); - - private DnsResult ResolveCNameCore(string name, CancellationToken cancellationToken) - => throw new PlatformNotSupportedException(); - - private DnsResult ResolvePtrCore(string name, CancellationToken cancellationToken) - => throw new PlatformNotSupportedException(); - - private DnsResult ResolveNsCore(string name, CancellationToken cancellationToken) + private Task> ResolveNsCore(bool async, string name, CancellationToken cancellationToken) => throw new PlatformNotSupportedException(); } } diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Windows.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Windows.cs index b6e16b1eaec498..35346a408038ef 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Windows.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Windows.cs @@ -13,80 +13,43 @@ namespace System.Net { public sealed partial class DnsResolver { - // ---- Asynchronous Resolve*Core methods (called from cross-platform DnsResolver) ---- - - private async Task> ResolveAddressesCoreAsync(string name, AddressFamily addressFamily, CancellationToken cancellationToken) + // ---- Resolve*Core methods (called from cross-platform DnsResolver) ---- + // + // Each method takes a `bool async` flag controlling whether the underlying + // DnsQueryEx call is issued asynchronously (via the completion-callback state + // machine) or synchronously (inline on the calling thread). When async is + // false the returned Task is already completed, so the synchronous public + // entry points can safely unwrap it without blocking a thread. + + private async Task> ResolveAddressesCore(bool async, string name, AddressFamily addressFamily, CancellationToken cancellationToken) { if (addressFamily == AddressFamily.Unspecified) { - // Issue A and AAAA in parallel; merge results. - Task> aTask = QueryAddressesAsync(name, Interop.Dnsapi.DNS_TYPE_A, cancellationToken); - Task> aaaaTask = QueryAddressesAsync(name, Interop.Dnsapi.DNS_TYPE_AAAA, cancellationToken); - DnsResult aRes = await aTask.ConfigureAwait(false); - DnsResult aaaaRes = await aaaaTask.ConfigureAwait(false); - return MergeAddressResults(aRes, aaaaRes); - } - - ushort qtype = AddressFamilyToQueryType(addressFamily); - return await QueryAddressesAsync(name, qtype, cancellationToken).ConfigureAwait(false); - } - - private async Task> ResolveSrvCoreAsync(string name, CancellationToken cancellationToken) - { - DnsQueryRawResult raw = await DnsQueryExAsync(name, Interop.Dnsapi.DNS_TYPE_SRV, cancellationToken).ConfigureAwait(false); - try - { - return ParseSrv(raw); - } - finally - { - raw.Dispose(); - } - } - - private Task> ResolveMxCoreAsync(string name, CancellationToken cancellationToken) - => QuerySimpleAsync(name, Interop.Dnsapi.DNS_TYPE_MX, cancellationToken, s_parseMx); - - private Task> ResolveCNameCoreAsync(string name, CancellationToken cancellationToken) - => QuerySimpleAsync(name, Interop.Dnsapi.DNS_TYPE_CNAME, cancellationToken, s_parseCName); - - private Task> ResolvePtrCoreAsync(string name, CancellationToken cancellationToken) - => QuerySimpleAsync(name, Interop.Dnsapi.DNS_TYPE_PTR, cancellationToken, s_parsePtr); - - private Task> ResolveNsCoreAsync(string name, CancellationToken cancellationToken) - => QuerySimpleAsync(name, Interop.Dnsapi.DNS_TYPE_NS, cancellationToken, s_parseNs); - - private async Task> ResolveTxtCoreAsync(string name, CancellationToken cancellationToken) - { - DnsQueryRawResult raw = await DnsQueryExAsync(name, Interop.Dnsapi.DNS_TYPE_TEXT, cancellationToken).ConfigureAwait(false); - try - { - return ParseTxt(raw); - } - finally - { - raw.Dispose(); - } - } - - // ---- Synchronous Resolve*Core methods (called from cross-platform DnsResolver) ---- - - private DnsResult ResolveAddressesCore(string name, AddressFamily addressFamily, CancellationToken cancellationToken) - { - if (addressFamily == AddressFamily.Unspecified) - { - DnsResult aRes = QueryAddresses(name, Interop.Dnsapi.DNS_TYPE_A, cancellationToken); - DnsResult aaaaRes = QueryAddresses(name, Interop.Dnsapi.DNS_TYPE_AAAA, cancellationToken); - return MergeAddressResults(aRes, aaaaRes); + if (async) + { + // Issue A and AAAA in parallel; merge results. + Task> aTask = QueryAddresses(async: true, name, Interop.Dnsapi.DNS_TYPE_A, cancellationToken); + Task> aaaaTask = QueryAddresses(async: true, name, Interop.Dnsapi.DNS_TYPE_AAAA, cancellationToken); + DnsResult aRes = await aTask.ConfigureAwait(false); + DnsResult aaaaRes = await aaaaTask.ConfigureAwait(false); + return MergeAddressResults(aRes, aaaaRes); + } + else + { + // Synchronous: query A then AAAA sequentially. + DnsResult aRes = await QueryAddresses(async: false, name, Interop.Dnsapi.DNS_TYPE_A, cancellationToken).ConfigureAwait(false); + DnsResult aaaaRes = await QueryAddresses(async: false, name, Interop.Dnsapi.DNS_TYPE_AAAA, cancellationToken).ConfigureAwait(false); + return MergeAddressResults(aRes, aaaaRes); + } } ushort qtype = AddressFamilyToQueryType(addressFamily); - return QueryAddresses(name, qtype, cancellationToken); + return await QueryAddresses(async, name, qtype, cancellationToken).ConfigureAwait(false); } - private DnsResult ResolveSrvCore(string name, CancellationToken cancellationToken) + private async Task> ResolveSrvCore(bool async, string name, CancellationToken cancellationToken) { - DnsQueryRawResult raw = DnsQueryExSync(name, Interop.Dnsapi.DNS_TYPE_SRV, cancellationToken); + DnsQueryRawResult raw = await DnsQueryEx(async, name, Interop.Dnsapi.DNS_TYPE_SRV, cancellationToken).ConfigureAwait(false); try { return ParseSrv(raw); @@ -97,21 +60,21 @@ private DnsResult ResolveSrvCore(string name, CancellationToken cance } } - private DnsResult ResolveMxCore(string name, CancellationToken cancellationToken) - => QuerySimple(name, Interop.Dnsapi.DNS_TYPE_MX, cancellationToken, s_parseMx); + private Task> ResolveMxCore(bool async, string name, CancellationToken cancellationToken) + => QuerySimple(async, name, Interop.Dnsapi.DNS_TYPE_MX, cancellationToken, s_parseMx); - private DnsResult ResolveCNameCore(string name, CancellationToken cancellationToken) - => QuerySimple(name, Interop.Dnsapi.DNS_TYPE_CNAME, cancellationToken, s_parseCName); + private Task> ResolveCNameCore(bool async, string name, CancellationToken cancellationToken) + => QuerySimple(async, name, Interop.Dnsapi.DNS_TYPE_CNAME, cancellationToken, s_parseCName); - private DnsResult ResolvePtrCore(string name, CancellationToken cancellationToken) - => QuerySimple(name, Interop.Dnsapi.DNS_TYPE_PTR, cancellationToken, s_parsePtr); + private Task> ResolvePtrCore(bool async, string name, CancellationToken cancellationToken) + => QuerySimple(async, name, Interop.Dnsapi.DNS_TYPE_PTR, cancellationToken, s_parsePtr); - private DnsResult ResolveNsCore(string name, CancellationToken cancellationToken) - => QuerySimple(name, Interop.Dnsapi.DNS_TYPE_NS, cancellationToken, s_parseNs); + private Task> ResolveNsCore(bool async, string name, CancellationToken cancellationToken) + => QuerySimple(async, name, Interop.Dnsapi.DNS_TYPE_NS, cancellationToken, s_parseNs); - private DnsResult ResolveTxtCore(string name, CancellationToken cancellationToken) + private async Task> ResolveTxtCore(bool async, string name, CancellationToken cancellationToken) { - DnsQueryRawResult raw = DnsQueryExSync(name, Interop.Dnsapi.DNS_TYPE_TEXT, cancellationToken); + DnsQueryRawResult raw = await DnsQueryEx(async, name, Interop.Dnsapi.DNS_TYPE_TEXT, cancellationToken).ConfigureAwait(false); try { return ParseTxt(raw); @@ -122,7 +85,7 @@ private DnsResult ResolveTxtCore(string name, CancellationToken cance } } - // ---- Per-record-type selectors (shared by sync and async paths) ---- + // ---- Per-record-type selectors (shared by all record types) ---- private static readonly Func s_parseMx = static (hdr, dataPtr) => { @@ -156,11 +119,11 @@ private static ushort AddressFamilyToQueryType(AddressFamily addressFamily) => _ => throw new ArgumentException(SR.net_invalid_ip_addr, nameof(addressFamily)), }; - // ---- Address query helpers ---- + // ---- Query wrappers (issue the query, then parse the record list) ---- - private async Task> QueryAddressesAsync(string name, ushort qtype, CancellationToken cancellationToken) + private async Task> QueryAddresses(bool async, string name, ushort qtype, CancellationToken cancellationToken) { - DnsQueryRawResult raw = await DnsQueryExAsync(name, qtype, cancellationToken).ConfigureAwait(false); + DnsQueryRawResult raw = await DnsQueryEx(async, name, qtype, cancellationToken).ConfigureAwait(false); try { return ParseAddresses(raw, qtype); @@ -171,12 +134,13 @@ private async Task> QueryAddressesAsync(string name, us } } - private DnsResult QueryAddresses(string name, ushort qtype, CancellationToken cancellationToken) + private async Task> QuerySimple(bool async, string name, ushort qtype, CancellationToken cancellationToken, + Func selector) { - DnsQueryRawResult raw = DnsQueryExSync(name, qtype, cancellationToken); + DnsQueryRawResult raw = await DnsQueryEx(async, name, qtype, cancellationToken).ConfigureAwait(false); try { - return ParseAddresses(raw, qtype); + return ParseSimple(raw, qtype, selector); } finally { @@ -184,7 +148,7 @@ private DnsResult QueryAddresses(string name, ushort qtype, Cance } } - // ---- Record-list parsers (sync/async agnostic) ---- + // ---- Record-list parsers ---- private static DnsResult ParseAddresses(DnsQueryRawResult raw, ushort qtype) { @@ -372,47 +336,24 @@ private static void ParseAdditionalAddresses(IntPtr head, ref Dictionary> QuerySimpleAsync(string name, ushort qtype, CancellationToken cancellationToken, - Func selector) - { - DnsQueryRawResult raw = await DnsQueryExAsync(name, qtype, cancellationToken).ConfigureAwait(false); - try - { - return ParseSimple(raw, qtype, selector); - } - finally - { - raw.Dispose(); - } - } + // ---- Core DnsQueryEx wrapper ---- - private DnsResult QuerySimple(string name, ushort qtype, CancellationToken cancellationToken, - Func selector) + private unsafe Task DnsQueryEx(bool async, string name, ushort queryType, CancellationToken cancellationToken) { - DnsQueryRawResult raw = DnsQueryExSync(name, qtype, cancellationToken); - try - { - return ParseSimple(raw, qtype, selector); - } - finally + if (cancellationToken.IsCancellationRequested) { - raw.Dispose(); + return Task.FromCanceled(cancellationToken); } - } - // ---- Core DnsQueryEx wrappers ---- - - private unsafe Task DnsQueryExAsync(string name, ushort queryType, CancellationToken cancellationToken) - { - if (cancellationToken.IsCancellationRequested) + if (async) { - return Task.FromCanceled(cancellationToken); + DnsQueryAsyncState state = new DnsQueryAsyncState(_options.Servers, name, queryType, cancellationToken); + return state.StartAsync(); } - DnsQueryAsyncState state = new DnsQueryAsyncState(_options.Servers, name, queryType, cancellationToken); - return state.StartAsync(); + // Synchronous: the result is produced inline, so the returned Task is + // already completed and the sync entry points unwrap it without blocking. + return Task.FromResult(DnsQueryExSync(name, queryType, cancellationToken)); } private static unsafe string? PtrToString(IntPtr p) => diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs index f47a53ebfd5123..f290a5609803d0 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs @@ -34,49 +34,49 @@ public DnsResult ResolveAddresses(string name, AddressFamily addr { ValidateName(name); ObjectDisposedException.ThrowIf(_disposed, this); - return ResolveAddressesCore(name, addressFamily, default); + return ResolveAddressesCore(async: false, name, addressFamily, default).GetAwaiter().GetResult(); } public DnsResult ResolveSrv(string name) { ValidateName(name); ObjectDisposedException.ThrowIf(_disposed, this); - return ResolveSrvCore(name, default); + return ResolveSrvCore(async: false, name, default).GetAwaiter().GetResult(); } public DnsResult ResolveMx(string name) { ValidateName(name); ObjectDisposedException.ThrowIf(_disposed, this); - return ResolveMxCore(name, default); + return ResolveMxCore(async: false, name, default).GetAwaiter().GetResult(); } public DnsResult ResolveTxt(string name) { ValidateName(name); ObjectDisposedException.ThrowIf(_disposed, this); - return ResolveTxtCore(name, default); + return ResolveTxtCore(async: false, name, default).GetAwaiter().GetResult(); } public DnsResult ResolveCName(string name) { ValidateName(name); ObjectDisposedException.ThrowIf(_disposed, this); - return ResolveCNameCore(name, default); + return ResolveCNameCore(async: false, name, default).GetAwaiter().GetResult(); } public DnsResult ResolvePtr(string name) { ValidateName(name); ObjectDisposedException.ThrowIf(_disposed, this); - return ResolvePtrCore(name, default); + return ResolvePtrCore(async: false, name, default).GetAwaiter().GetResult(); } public DnsResult ResolveNs(string name) { ValidateName(name); ObjectDisposedException.ThrowIf(_disposed, this); - return ResolveNsCore(name, default); + return ResolveNsCore(async: false, name, default).GetAwaiter().GetResult(); } public Task> ResolveAddressesAsync(string name, CancellationToken cancellationToken = default) @@ -86,56 +86,56 @@ public Task> ResolveAddressesAsync(string name, Address { ValidateName(name); ObjectDisposedException.ThrowIf(_disposed, this); - return ResolveAddressesCoreAsync(name, addressFamily, cancellationToken); + return ResolveAddressesCore(async: true, name, addressFamily, cancellationToken); } public Task> ResolveSrvAsync(string name, CancellationToken cancellationToken = default) { ValidateName(name); ObjectDisposedException.ThrowIf(_disposed, this); - return ResolveSrvCoreAsync(name, cancellationToken); + return ResolveSrvCore(async: true, name, cancellationToken); } public Task> ResolveMxAsync(string name, CancellationToken cancellationToken = default) { ValidateName(name); ObjectDisposedException.ThrowIf(_disposed, this); - return ResolveMxCoreAsync(name, cancellationToken); + return ResolveMxCore(async: true, name, cancellationToken); } public Task> ResolveTxtAsync(string name, CancellationToken cancellationToken = default) { ValidateName(name); ObjectDisposedException.ThrowIf(_disposed, this); - return ResolveTxtCoreAsync(name, cancellationToken); + return ResolveTxtCore(async: true, name, cancellationToken); } public Task> ResolveCNameAsync(string name, CancellationToken cancellationToken = default) { ValidateName(name); ObjectDisposedException.ThrowIf(_disposed, this); - return ResolveCNameCoreAsync(name, cancellationToken); + return ResolveCNameCore(async: true, name, cancellationToken); } public Task> ResolvePtrAsync(string name, CancellationToken cancellationToken = default) { ValidateName(name); ObjectDisposedException.ThrowIf(_disposed, this); - return ResolvePtrCoreAsync(name, cancellationToken); + return ResolvePtrCore(async: true, name, cancellationToken); } public Task> ResolvePtrAsync(IPAddress address, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(address); ObjectDisposedException.ThrowIf(_disposed, this); - return ResolvePtrCoreAsync(BuildArpaName(address), cancellationToken); + return ResolvePtrCore(async: true, BuildArpaName(address), cancellationToken); } public Task> ResolveNsAsync(string name, CancellationToken cancellationToken = default) { ValidateName(name); ObjectDisposedException.ThrowIf(_disposed, this); - return ResolveNsCoreAsync(name, cancellationToken); + return ResolveNsCore(async: true, name, cancellationToken); } public void Dispose() => _disposed = true; From 8196da616106499b8858050ff1eaccd274083704 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Thu, 11 Jun 2026 15:11:58 +0200 Subject: [PATCH 07/19] Assert sync DnsResolver core tasks complete synchronously Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/Net/DnsResolver.cs | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs index f290a5609803d0..29da9b9260d38c 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; @@ -34,49 +35,63 @@ public DnsResult ResolveAddresses(string name, AddressFamily addr { ValidateName(name); ObjectDisposedException.ThrowIf(_disposed, this); - return ResolveAddressesCore(async: false, name, addressFamily, default).GetAwaiter().GetResult(); + Task> task = ResolveAddressesCore(async: false, name, addressFamily, default); + Debug.Assert(task.IsCompleted); + return task.GetAwaiter().GetResult(); } public DnsResult ResolveSrv(string name) { ValidateName(name); ObjectDisposedException.ThrowIf(_disposed, this); - return ResolveSrvCore(async: false, name, default).GetAwaiter().GetResult(); + Task> task = ResolveSrvCore(async: false, name, default); + Debug.Assert(task.IsCompleted); + return task.GetAwaiter().GetResult(); } public DnsResult ResolveMx(string name) { ValidateName(name); ObjectDisposedException.ThrowIf(_disposed, this); - return ResolveMxCore(async: false, name, default).GetAwaiter().GetResult(); + Task> task = ResolveMxCore(async: false, name, default); + Debug.Assert(task.IsCompleted); + return task.GetAwaiter().GetResult(); } public DnsResult ResolveTxt(string name) { ValidateName(name); ObjectDisposedException.ThrowIf(_disposed, this); - return ResolveTxtCore(async: false, name, default).GetAwaiter().GetResult(); + Task> task = ResolveTxtCore(async: false, name, default); + Debug.Assert(task.IsCompleted); + return task.GetAwaiter().GetResult(); } public DnsResult ResolveCName(string name) { ValidateName(name); ObjectDisposedException.ThrowIf(_disposed, this); - return ResolveCNameCore(async: false, name, default).GetAwaiter().GetResult(); + Task> task = ResolveCNameCore(async: false, name, default); + Debug.Assert(task.IsCompleted); + return task.GetAwaiter().GetResult(); } public DnsResult ResolvePtr(string name) { ValidateName(name); ObjectDisposedException.ThrowIf(_disposed, this); - return ResolvePtrCore(async: false, name, default).GetAwaiter().GetResult(); + Task> task = ResolvePtrCore(async: false, name, default); + Debug.Assert(task.IsCompleted); + return task.GetAwaiter().GetResult(); } public DnsResult ResolveNs(string name) { ValidateName(name); ObjectDisposedException.ThrowIf(_disposed, this); - return ResolveNsCore(async: false, name, default).GetAwaiter().GetResult(); + Task> task = ResolveNsCore(async: false, name, default); + Debug.Assert(task.IsCompleted); + return task.GetAwaiter().GetResult(); } public Task> ResolveAddressesAsync(string name, CancellationToken cancellationToken = default) From 522c3968be9ba134bd184ea7ee1fb41b244fded1 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Thu, 11 Jun 2026 17:26:02 +0200 Subject: [PATCH 08/19] Refactor Windows DnsResolver logic into DnsResolverPal (PAL pattern) 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> --- .../src/System.Net.NameResolution.csproj | 7 +- .../src/System/Net/DnsResolver.Unsupported.cs | 35 - .../src/System/Net/DnsResolver.Windows.cs | 386 --------- .../System/Net/DnsResolver.WindowsAsync.cs | 429 ---------- .../src/System/Net/DnsResolver.cs | 30 + .../System/Net/DnsResolverPal.Unsupported.cs | 34 + .../src/System/Net/DnsResolverPal.Windows.cs | 801 ++++++++++++++++++ 7 files changed, 868 insertions(+), 854 deletions(-) delete mode 100644 src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Unsupported.cs delete mode 100644 src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Windows.cs delete mode 100644 src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.WindowsAsync.cs create mode 100644 src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Unsupported.cs create mode 100644 src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs diff --git a/src/libraries/System.Net.NameResolution/src/System.Net.NameResolution.csproj b/src/libraries/System.Net.NameResolution/src/System.Net.NameResolution.csproj index 89da5be9e94bf0..4cdb503467afc2 100644 --- a/src/libraries/System.Net.NameResolution/src/System.Net.NameResolution.csproj +++ b/src/libraries/System.Net.NameResolution/src/System.Net.NameResolution.csproj @@ -43,8 +43,7 @@ - - + @@ -89,7 +88,7 @@ - + - + > ResolveAddressesCore(bool async, string name, AddressFamily addressFamily, CancellationToken cancellationToken) - => throw new PlatformNotSupportedException(); - - private Task> ResolveSrvCore(bool async, string name, CancellationToken cancellationToken) - => throw new PlatformNotSupportedException(); - - private Task> ResolveMxCore(bool async, string name, CancellationToken cancellationToken) - => throw new PlatformNotSupportedException(); - - private Task> ResolveTxtCore(bool async, string name, CancellationToken cancellationToken) - => throw new PlatformNotSupportedException(); - - private Task> ResolveCNameCore(bool async, string name, CancellationToken cancellationToken) - => throw new PlatformNotSupportedException(); - - private Task> ResolvePtrCore(bool async, string name, CancellationToken cancellationToken) - => throw new PlatformNotSupportedException(); - - private Task> ResolveNsCore(bool async, string name, CancellationToken cancellationToken) - => throw new PlatformNotSupportedException(); - } -} diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Windows.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Windows.cs deleted file mode 100644 index 35346a408038ef..00000000000000 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.Windows.cs +++ /dev/null @@ -1,386 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Buffers.Binary; -using System.Collections.Generic; -using System.Diagnostics; -using System.Net.Sockets; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; - -namespace System.Net -{ - public sealed partial class DnsResolver - { - // ---- Resolve*Core methods (called from cross-platform DnsResolver) ---- - // - // Each method takes a `bool async` flag controlling whether the underlying - // DnsQueryEx call is issued asynchronously (via the completion-callback state - // machine) or synchronously (inline on the calling thread). When async is - // false the returned Task is already completed, so the synchronous public - // entry points can safely unwrap it without blocking a thread. - - private async Task> ResolveAddressesCore(bool async, string name, AddressFamily addressFamily, CancellationToken cancellationToken) - { - if (addressFamily == AddressFamily.Unspecified) - { - if (async) - { - // Issue A and AAAA in parallel; merge results. - Task> aTask = QueryAddresses(async: true, name, Interop.Dnsapi.DNS_TYPE_A, cancellationToken); - Task> aaaaTask = QueryAddresses(async: true, name, Interop.Dnsapi.DNS_TYPE_AAAA, cancellationToken); - DnsResult aRes = await aTask.ConfigureAwait(false); - DnsResult aaaaRes = await aaaaTask.ConfigureAwait(false); - return MergeAddressResults(aRes, aaaaRes); - } - else - { - // Synchronous: query A then AAAA sequentially. - DnsResult aRes = await QueryAddresses(async: false, name, Interop.Dnsapi.DNS_TYPE_A, cancellationToken).ConfigureAwait(false); - DnsResult aaaaRes = await QueryAddresses(async: false, name, Interop.Dnsapi.DNS_TYPE_AAAA, cancellationToken).ConfigureAwait(false); - return MergeAddressResults(aRes, aaaaRes); - } - } - - ushort qtype = AddressFamilyToQueryType(addressFamily); - return await QueryAddresses(async, name, qtype, cancellationToken).ConfigureAwait(false); - } - - private async Task> ResolveSrvCore(bool async, string name, CancellationToken cancellationToken) - { - DnsQueryRawResult raw = await DnsQueryEx(async, name, Interop.Dnsapi.DNS_TYPE_SRV, cancellationToken).ConfigureAwait(false); - try - { - return ParseSrv(raw); - } - finally - { - raw.Dispose(); - } - } - - private Task> ResolveMxCore(bool async, string name, CancellationToken cancellationToken) - => QuerySimple(async, name, Interop.Dnsapi.DNS_TYPE_MX, cancellationToken, s_parseMx); - - private Task> ResolveCNameCore(bool async, string name, CancellationToken cancellationToken) - => QuerySimple(async, name, Interop.Dnsapi.DNS_TYPE_CNAME, cancellationToken, s_parseCName); - - private Task> ResolvePtrCore(bool async, string name, CancellationToken cancellationToken) - => QuerySimple(async, name, Interop.Dnsapi.DNS_TYPE_PTR, cancellationToken, s_parsePtr); - - private Task> ResolveNsCore(bool async, string name, CancellationToken cancellationToken) - => QuerySimple(async, name, Interop.Dnsapi.DNS_TYPE_NS, cancellationToken, s_parseNs); - - private async Task> ResolveTxtCore(bool async, string name, CancellationToken cancellationToken) - { - DnsQueryRawResult raw = await DnsQueryEx(async, name, Interop.Dnsapi.DNS_TYPE_TEXT, cancellationToken).ConfigureAwait(false); - try - { - return ParseTxt(raw); - } - finally - { - raw.Dispose(); - } - } - - // ---- Per-record-type selectors (shared by all record types) ---- - - private static readonly Func s_parseMx = static (hdr, dataPtr) => - { - Interop.Dnsapi.DNS_MX_DATA data = Marshal.PtrToStructure(dataPtr); - return new MxRecord(PtrToString(data.pNameExchange) ?? string.Empty, data.wPreference, TimeSpan.FromSeconds(hdr.dwTtl)); - }; - - private static readonly Func s_parseCName = static (hdr, dataPtr) => - { - Interop.Dnsapi.DNS_CNAME_DATA data = Marshal.PtrToStructure(dataPtr); - return new CNameRecord(PtrToString(data.pNameHost) ?? string.Empty, TimeSpan.FromSeconds(hdr.dwTtl)); - }; - - private static readonly Func s_parsePtr = static (hdr, dataPtr) => - { - Interop.Dnsapi.DNS_PTR_DATA data = Marshal.PtrToStructure(dataPtr); - return new PtrRecord(PtrToString(data.pNameHost) ?? string.Empty, TimeSpan.FromSeconds(hdr.dwTtl)); - }; - - private static readonly Func s_parseNs = static (hdr, dataPtr) => - { - Interop.Dnsapi.DNS_NS_DATA data = Marshal.PtrToStructure(dataPtr); - return new NsRecord(PtrToString(data.pNameHost) ?? string.Empty, TimeSpan.FromSeconds(hdr.dwTtl)); - }; - - private static ushort AddressFamilyToQueryType(AddressFamily addressFamily) => - addressFamily switch - { - AddressFamily.InterNetwork => Interop.Dnsapi.DNS_TYPE_A, - AddressFamily.InterNetworkV6 => Interop.Dnsapi.DNS_TYPE_AAAA, - _ => throw new ArgumentException(SR.net_invalid_ip_addr, nameof(addressFamily)), - }; - - // ---- Query wrappers (issue the query, then parse the record list) ---- - - private async Task> QueryAddresses(bool async, string name, ushort qtype, CancellationToken cancellationToken) - { - DnsQueryRawResult raw = await DnsQueryEx(async, name, qtype, cancellationToken).ConfigureAwait(false); - try - { - return ParseAddresses(raw, qtype); - } - finally - { - raw.Dispose(); - } - } - - private async Task> QuerySimple(bool async, string name, ushort qtype, CancellationToken cancellationToken, - Func selector) - { - DnsQueryRawResult raw = await DnsQueryEx(async, name, qtype, cancellationToken).ConfigureAwait(false); - try - { - return ParseSimple(raw, qtype, selector); - } - finally - { - raw.Dispose(); - } - } - - // ---- Record-list parsers ---- - - private static DnsResult ParseAddresses(DnsQueryRawResult raw, ushort qtype) - { - if (raw.ResponseCode != DnsResponseCode.NoError) - { - return new DnsResult(raw.ResponseCode, null, raw.NegativeCacheTtl); - } - - List records = new(); - for (IntPtr cur = raw.RecordsHead; cur != IntPtr.Zero; ) - { - Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); - uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; - if (hdr.wType == qtype && section == Interop.Dnsapi.DNSREC_ANSWER) - { - IntPtr dataPtr = cur + Marshal.SizeOf(); - if (TryParseAddress(hdr.wType, dataPtr, out IPAddress? address)) - { - records.Add(new AddressRecord(address!, TimeSpan.FromSeconds(hdr.dwTtl))); - } - } - cur = hdr.pNext; - } - - return new DnsResult(DnsResponseCode.NoError, records, TimeSpan.Zero); - } - - private static DnsResult ParseSrv(DnsQueryRawResult raw) - { - if (raw.ResponseCode != DnsResponseCode.NoError) - { - return new DnsResult(raw.ResponseCode, null, raw.NegativeCacheTtl); - } - - // Gather additional-section A/AAAA records by name so we can attach them. - Dictionary>? glue = null; - ParseAdditionalAddresses(raw.RecordsHead, ref glue); - - List records = new(); - for (IntPtr cur = raw.RecordsHead; cur != IntPtr.Zero; ) - { - Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); - uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; - if (hdr.wType == Interop.Dnsapi.DNS_TYPE_SRV && section == Interop.Dnsapi.DNSREC_ANSWER) - { - IntPtr dataPtr = cur + Marshal.SizeOf(); - Interop.Dnsapi.DNS_SRV_DATA data = Marshal.PtrToStructure(dataPtr); - string target = PtrToString(data.pNameTarget) ?? string.Empty; - IReadOnlyList? attached = null; - if (glue != null && glue.TryGetValue(target, out List? list)) - { - attached = list; - } - records.Add(new SrvRecord(target, data.wPort, data.wPriority, data.wWeight, TimeSpan.FromSeconds(hdr.dwTtl), attached)); - } - cur = hdr.pNext; - } - - return new DnsResult(DnsResponseCode.NoError, records, TimeSpan.Zero); - } - - private static DnsResult ParseTxt(DnsQueryRawResult raw) - { - if (raw.ResponseCode != DnsResponseCode.NoError) - { - return new DnsResult(raw.ResponseCode, null, raw.NegativeCacheTtl); - } - - List records = new(); - for (IntPtr cur = raw.RecordsHead; cur != IntPtr.Zero; ) - { - Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); - uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; - if (hdr.wType == Interop.Dnsapi.DNS_TYPE_TEXT && section == Interop.Dnsapi.DNSREC_ANSWER) - { - IntPtr dataPtr = cur + Marshal.SizeOf(); - // DNS_TXT_DATA: uint dwStringCount; followed by array of PCWSTR. - uint count = (uint)Marshal.ReadInt32(dataPtr); - int ptrSize = IntPtr.Size; - IntPtr stringsPtr = dataPtr + sizeof(uint); - if (ptrSize > sizeof(uint)) - { - // Round up to pointer alignment. - long aligned = ((long)stringsPtr + (ptrSize - 1)) & ~(long)(ptrSize - 1); - stringsPtr = checked((nint)aligned); - } - string[] values = new string[count]; - for (int i = 0; i < count; i++) - { - IntPtr strPtr = Marshal.ReadIntPtr(stringsPtr, i * ptrSize); - values[i] = PtrToString(strPtr) ?? string.Empty; - } - records.Add(new TxtRecord(values, TimeSpan.FromSeconds(hdr.dwTtl))); - } - cur = hdr.pNext; - } - - return new DnsResult(DnsResponseCode.NoError, records, TimeSpan.Zero); - } - - private static DnsResult ParseSimple(DnsQueryRawResult raw, ushort qtype, - Func selector) - { - if (raw.ResponseCode != DnsResponseCode.NoError) - { - return new DnsResult(raw.ResponseCode, null, raw.NegativeCacheTtl); - } - - List records = new(); - for (IntPtr cur = raw.RecordsHead; cur != IntPtr.Zero; ) - { - Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); - uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; - if (hdr.wType == qtype && section == Interop.Dnsapi.DNSREC_ANSWER) - { - IntPtr dataPtr = cur + Marshal.SizeOf(); - records.Add(selector(hdr, dataPtr)); - } - cur = hdr.pNext; - } - - return new DnsResult(DnsResponseCode.NoError, records, TimeSpan.Zero); - } - - private static DnsResult MergeAddressResults(DnsResult a, DnsResult b) - { - if (a.Records.Count > 0 || b.Records.Count > 0) - { - AddressRecord[] merged = new AddressRecord[a.Records.Count + b.Records.Count]; - int idx = 0; - for (int i = 0; i < a.Records.Count; i++) merged[idx++] = a.Records[i]; - for (int i = 0; i < b.Records.Count; i++) merged[idx++] = b.Records[i]; - return new DnsResult(DnsResponseCode.NoError, merged, TimeSpan.Zero); - } - - DnsResponseCode chosenRc = a.ResponseCode == DnsResponseCode.NxDomain || b.ResponseCode == DnsResponseCode.NxDomain - ? DnsResponseCode.NxDomain - : (a.ResponseCode != DnsResponseCode.NoError ? a.ResponseCode : b.ResponseCode); - TimeSpan negTtl = a.NegativeCacheTtl > TimeSpan.Zero ? a.NegativeCacheTtl : b.NegativeCacheTtl; - return new DnsResult(chosenRc, null, negTtl); - } - - private static bool TryParseAddress(ushort recordType, IntPtr dataPtr, out IPAddress? address) - { - if (recordType == Interop.Dnsapi.DNS_TYPE_A) - { - uint ip = (uint)Marshal.ReadInt32(dataPtr); - address = new IPAddress(ip); - return true; - } - if (recordType == Interop.Dnsapi.DNS_TYPE_AAAA) - { - byte[] bytes = new byte[16]; - Marshal.Copy(dataPtr, bytes, 0, 16); - address = new IPAddress(bytes); - return true; - } - address = null; - return false; - } - - private static void ParseAdditionalAddresses(IntPtr head, ref Dictionary>? glue) - { - for (IntPtr cur = head; cur != IntPtr.Zero; ) - { - Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); - uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; - bool isAddress = hdr.wType == Interop.Dnsapi.DNS_TYPE_A || hdr.wType == Interop.Dnsapi.DNS_TYPE_AAAA; - if (section == Interop.Dnsapi.DNSREC_ADDITIONAL && isAddress) - { - IntPtr dataPtr = cur + Marshal.SizeOf(); - if (TryParseAddress(hdr.wType, dataPtr, out IPAddress? address)) - { - string name = PtrToString(hdr.pName) ?? string.Empty; - glue ??= new Dictionary>(StringComparer.OrdinalIgnoreCase); - if (!glue.TryGetValue(name, out List? list)) - { - list = new List(); - glue[name] = list; - } - list.Add(new AddressRecord(address!, TimeSpan.FromSeconds(hdr.dwTtl))); - } - } - cur = hdr.pNext; - } - } - - // ---- Core DnsQueryEx wrapper ---- - - private unsafe Task DnsQueryEx(bool async, string name, ushort queryType, CancellationToken cancellationToken) - { - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); - } - - if (async) - { - DnsQueryAsyncState state = new DnsQueryAsyncState(_options.Servers, name, queryType, cancellationToken); - return state.StartAsync(); - } - - // Synchronous: the result is produced inline, so the returned Task is - // already completed and the sync entry points unwrap it without blocking. - return Task.FromResult(DnsQueryExSync(name, queryType, cancellationToken)); - } - - private static unsafe string? PtrToString(IntPtr p) => - p == IntPtr.Zero ? null : Marshal.PtrToStringUni(p); - - // ---- Raw query result returned by the low-level helpers ---- - - private readonly struct DnsQueryRawResult : IDisposable - { - public DnsResponseCode ResponseCode { get; } - public IntPtr RecordsHead { get; } - public TimeSpan NegativeCacheTtl { get; } - - public DnsQueryRawResult(DnsResponseCode responseCode, IntPtr recordsHead, TimeSpan negativeCacheTtl) - { - ResponseCode = responseCode; - RecordsHead = recordsHead; - NegativeCacheTtl = negativeCacheTtl; - } - - public void Dispose() - { - if (RecordsHead != IntPtr.Zero) - { - Interop.Dnsapi.DnsFree(RecordsHead, Interop.Dnsapi.DnsFreeRecordList); - } - } - } - } -} diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.WindowsAsync.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.WindowsAsync.cs deleted file mode 100644 index 326e14a783754b..00000000000000 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.WindowsAsync.cs +++ /dev/null @@ -1,429 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Diagnostics; -using System.Net.Sockets; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; - -namespace System.Net -{ - public sealed partial class DnsResolver - { - // Cached callback so we don't allocate a new delegate per query. - private static readonly Interop.Dnsapi.DnsQueryCompletionRoutine s_completionCallback = QueryCompletionCallback; - private static readonly IntPtr s_completionCallbackPtr = - Marshal.GetFunctionPointerForDelegate(s_completionCallback); - - /// - /// Holds the unmanaged state for a single DnsQueryEx invocation, including - /// the request/result/cancel structures, the pinned query name, and the - /// completion TaskCompletionSource. - /// - private sealed unsafe class DnsQueryAsyncState - { - private readonly TaskCompletionSource _tcs = - new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - private readonly string _name; - private readonly ushort _queryType; - private readonly CancellationToken _cancellationToken; - private readonly IList _servers; - - private GCHandle _selfHandle; - private IntPtr _namePtr; - private IntPtr _requestPtr; - private IntPtr _resultPtr; - private IntPtr _cancelPtr; - private IntPtr _serverListPtr; // DNS_ADDR_ARRAY buffer - private CancellationTokenRegistration _ctReg; - private int _completed; // 0 = pending, 1 = completed (callback or sync) - - public DnsQueryAsyncState(IList servers, string name, ushort queryType, CancellationToken cancellationToken) - { - _servers = servers; - _name = name; - _queryType = queryType; - _cancellationToken = cancellationToken; - } - - public Task StartAsync() - { - ValidateServerPorts(_servers); - - try - { - _namePtr = Marshal.StringToHGlobalUni(_name); - _resultPtr = Marshal.AllocHGlobal(sizeof(Interop.Dnsapi.DNS_QUERY_RESULT)); - NativeMemory.Clear((void*)_resultPtr, (nuint)sizeof(Interop.Dnsapi.DNS_QUERY_RESULT)); - Interop.Dnsapi.DNS_QUERY_RESULT* result = (Interop.Dnsapi.DNS_QUERY_RESULT*)_resultPtr; - result->Version = Interop.Dnsapi.DNS_QUERY_REQUEST_VERSION1; - - _cancelPtr = Marshal.AllocHGlobal(sizeof(Interop.Dnsapi.DNS_QUERY_CANCEL)); - NativeMemory.Clear((void*)_cancelPtr, (nuint)sizeof(Interop.Dnsapi.DNS_QUERY_CANCEL)); - - _selfHandle = GCHandle.Alloc(this); - - _requestPtr = Marshal.AllocHGlobal(sizeof(Interop.Dnsapi.DNS_QUERY_REQUEST)); - NativeMemory.Clear((void*)_requestPtr, (nuint)sizeof(Interop.Dnsapi.DNS_QUERY_REQUEST)); - Interop.Dnsapi.DNS_QUERY_REQUEST* req = (Interop.Dnsapi.DNS_QUERY_REQUEST*)_requestPtr; - req->Version = Interop.Dnsapi.DNS_QUERY_REQUEST_VERSION1; - req->QueryName = _namePtr; - req->QueryType = _queryType; - req->QueryOptions = Interop.Dnsapi.DNS_QUERY_STANDARD; - req->InterfaceIndex = 0; - req->pQueryCompletionCallback = s_completionCallbackPtr; - req->pQueryContext = GCHandle.ToIntPtr(_selfHandle); - - if (_servers is { Count: > 0 }) - { - BuildAddrArray(_servers, out _serverListPtr); - req->pDnsServerList = (Interop.Dnsapi.DNS_ADDR_ARRAY*)_serverListPtr; - } - - int status = Interop.Dnsapi.DnsQueryEx( - (Interop.Dnsapi.DNS_QUERY_REQUEST*)_requestPtr, - (Interop.Dnsapi.DNS_QUERY_RESULT*)_resultPtr, - (Interop.Dnsapi.DNS_QUERY_CANCEL*)_cancelPtr); - - if (status == Interop.Dnsapi.DNS_REQUEST_PENDING) - { - // Async. Register cancellation; the callback will free resources and complete the TCS. - if (_cancellationToken.CanBeCanceled) - { - _ctReg = _cancellationToken.UnsafeRegister(static (s, _) => - { - DnsQueryAsyncState st = (DnsQueryAsyncState)s!; - st.CancelAndAbort(); - }, this); - } - } - else - { - // Synchronous completion. The callback was NOT invoked; we complete inline. - CompleteFromResult(status); - } - } - catch - { - FreeAll(); - throw; - } - - return _tcs.Task; - } - - private void CancelAndAbort() - { - if (_cancelPtr != IntPtr.Zero) - { - Interop.Dnsapi.DnsCancelQuery((Interop.Dnsapi.DNS_QUERY_CANCEL*)_cancelPtr); - } - } - - /// - /// Invoked from either the native callback or the sync completion path. - /// Parses the QueryStatus and pQueryRecords from the result struct, - /// completes the TCS, and frees state. - /// - internal void CompleteFromResult(int status) - { - if (Interlocked.Exchange(ref _completed, 1) != 0) - { - return; - } - - try - { - _ctReg.Dispose(); - - Interop.Dnsapi.DNS_QUERY_RESULT result = Marshal.PtrToStructure(_resultPtr); - IntPtr records = result.pQueryRecords; - - if (_cancellationToken.IsCancellationRequested) - { - if (records != IntPtr.Zero) - { - Interop.Dnsapi.DnsFree(records, Interop.Dnsapi.DnsFreeRecordList); - } - _tcs.TrySetCanceled(_cancellationToken); - return; - } - - DnsResponseCode rc = MapWindowsErrorToResponseCode(status); - - // 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); - } - - _tcs.TrySetResult(new DnsQueryRawResult(rc, records, negativeTtl)); - } - catch (Exception ex) - { - _tcs.TrySetException(ex); - } - finally - { - FreeAll(); - } - } - - private void FreeAll() - { - if (_namePtr != IntPtr.Zero) - { - Marshal.FreeHGlobal(_namePtr); - _namePtr = IntPtr.Zero; - } - if (_requestPtr != IntPtr.Zero) - { - Marshal.FreeHGlobal(_requestPtr); - _requestPtr = IntPtr.Zero; - } - if (_resultPtr != IntPtr.Zero) - { - Marshal.FreeHGlobal(_resultPtr); - _resultPtr = IntPtr.Zero; - } - if (_cancelPtr != IntPtr.Zero) - { - Marshal.FreeHGlobal(_cancelPtr); - _cancelPtr = IntPtr.Zero; - } - if (_serverListPtr != IntPtr.Zero) - { - Marshal.FreeHGlobal(_serverListPtr); - _serverListPtr = IntPtr.Zero; - } - if (_selfHandle.IsAllocated) - { - _selfHandle.Free(); - } - } - } - - // Native callback. Marshaled to a function pointer once at startup. - // We use a managed delegate (no UnmanagedCallersOnly) because callers - // currently pass it via Marshal.GetFunctionPointerForDelegate. - private static void QueryCompletionCallback(IntPtr pQueryContext, IntPtr pQueryResults) - { - try - { - GCHandle handle = GCHandle.FromIntPtr(pQueryContext); - DnsQueryAsyncState? state = handle.Target as DnsQueryAsyncState; - if (state == null) - { - return; - } - - // pQueryResults points to the same DNS_QUERY_RESULT we passed in. - unsafe - { - Interop.Dnsapi.DNS_QUERY_RESULT* res = (Interop.Dnsapi.DNS_QUERY_RESULT*)pQueryResults; - state.CompleteFromResult(res->QueryStatus); - } - } - catch - { - // Swallow — never allow exceptions to propagate into native code. - } - } - - // DnsQueryEx only supports DNS servers reachable on the standard port 53. - // The sockaddr port field passed to the API must be 0 (the API always - // queries port 53); supplying any non-zero port - even 53 itself - results - // in ERROR_INVALID_PARAMETER. We therefore reject any server endpoint that - // requests a non-default port, since it cannot be honored on Windows. - private static void ValidateServerPorts(IList servers) - { - if (servers is { Count: > 0 }) - { - foreach (IPEndPoint ep in servers) - { - if (ep.Port != 0 && ep.Port != 53) - { - throw new PlatformNotSupportedException(SR.net_dns_custom_port_not_supported); - } - } - } - } - - // Synchronous DnsQueryEx invocation. By omitting the completion callback the - // API executes the query inline on the calling thread and returns the result - // directly, so no GCHandle / TaskCompletionSource bookkeeping is required. - private unsafe DnsQueryRawResult DnsQueryExSync(string name, ushort queryType, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - ValidateServerPorts(_options.Servers); - - IntPtr namePtr = IntPtr.Zero; - IntPtr serverListPtr = IntPtr.Zero; - try - { - namePtr = Marshal.StringToHGlobalUni(name); - - Interop.Dnsapi.DNS_QUERY_RESULT result = default; - result.Version = Interop.Dnsapi.DNS_QUERY_REQUEST_VERSION1; - - Interop.Dnsapi.DNS_QUERY_REQUEST request = default; - request.Version = Interop.Dnsapi.DNS_QUERY_REQUEST_VERSION1; - request.QueryName = namePtr; - request.QueryType = queryType; - request.QueryOptions = Interop.Dnsapi.DNS_QUERY_STANDARD; - // No completion callback => synchronous execution. - - if (_options.Servers is { Count: > 0 }) - { - BuildAddrArray(_options.Servers, out serverListPtr); - request.pDnsServerList = (Interop.Dnsapi.DNS_ADDR_ARRAY*)serverListPtr; - } - - // A null cancel handle is valid for synchronous queries. - int status = Interop.Dnsapi.DnsQueryEx(&request, &result, null); - - IntPtr records = result.pQueryRecords; - - if (cancellationToken.IsCancellationRequested) - { - if (records != IntPtr.Zero) - { - Interop.Dnsapi.DnsFree(records, Interop.Dnsapi.DnsFreeRecordList); - } - throw new OperationCanceledException(cancellationToken); - } - - DnsResponseCode rc = MapWindowsErrorToResponseCode(status); - - // 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); - } - - return new DnsQueryRawResult(rc, records, negativeTtl); - } - finally - { - if (namePtr != IntPtr.Zero) - { - Marshal.FreeHGlobal(namePtr); - } - if (serverListPtr != IntPtr.Zero) - { - Marshal.FreeHGlobal(serverListPtr); - } - } - } - - private static unsafe void BuildAddrArray(IList servers, out IntPtr arrayPtr) - { - int count = servers.Count; - int headerSize = sizeof(Interop.Dnsapi.DNS_ADDR_ARRAY); - int addrSize = sizeof(Interop.Dnsapi.DNS_ADDR); - int totalSize = headerSize + addrSize * count; - - arrayPtr = Marshal.AllocHGlobal(totalSize); - NativeMemory.Clear((void*)arrayPtr, (nuint)totalSize); - - Interop.Dnsapi.DNS_ADDR_ARRAY* arr = (Interop.Dnsapi.DNS_ADDR_ARRAY*)arrayPtr; - arr->MaxCount = (uint)count; - arr->AddrCount = (uint)count; - arr->Family = (ushort)(servers[0].AddressFamily == AddressFamily.InterNetwork ? Interop.Dnsapi.AF_INET : Interop.Dnsapi.AF_INET6); - - byte* addrBase = (byte*)arrayPtr + headerSize; - for (int i = 0; i < count; i++) - { - IPEndPoint ep = servers[i]; - byte* sa = addrBase + (i * addrSize); - WriteSockAddr(sa, ep); - } - } - - // 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) - { - // DnsQueryEx always queries DNS servers on port 53 and requires the sockaddr - // port field to be left as 0. Supplying a non-zero port (even 53) is rejected - // with ERROR_INVALID_PARAMETER. Non-default ports are validated and rejected - // earlier in StartAsync, so the port is always written as 0 here. - if (ep.AddressFamily == AddressFamily.InterNetwork) - { - // sockaddr_in: ushort family, ushort port (net order), uint addr, 8 bytes zero - *(ushort*)(dest + 0) = Interop.Dnsapi.AF_INET; - // dest[2..3] (port) left zero - Span 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]; - // dest[8..15] left zero - } - else if (ep.AddressFamily == AddressFamily.InterNetworkV6) - { - // sockaddr_in6: ushort family, ushort port, uint flowinfo, 16-byte addr, uint scope_id - *(ushort*)(dest + 0) = Interop.Dnsapi.AF_INET6; - // dest[2..3] (port) left zero - // flowinfo (dest[4..7]) left zero - Span addrBytes = stackalloc byte[16]; - ep.Address.TryWriteBytes(addrBytes, out _); - for (int i = 0; i < 16; i++) - { - dest[8 + i] = addrBytes[i]; - } - // scope_id (dest[24..27]) - uint scopeId = (uint)ep.Address.ScopeId; - dest[24] = (byte)(scopeId & 0xff); - dest[25] = (byte)((scopeId >> 8) & 0xff); - dest[26] = (byte)((scopeId >> 16) & 0xff); - dest[27] = (byte)((scopeId >> 24) & 0xff); - } - else - { - throw new ArgumentException(SR.net_invalid_ip_addr); - } - } - - private static DnsResponseCode MapWindowsErrorToResponseCode(int status) => - status switch - { - Interop.Dnsapi.ERROR_SUCCESS => DnsResponseCode.NoError, - Interop.Dnsapi.DNS_INFO_NO_RECORDS => DnsResponseCode.NoError, // NODATA: name exists but no records of requested type - Interop.Dnsapi.DNS_ERROR_RCODE_NAME_ERROR => DnsResponseCode.NxDomain, - Interop.Dnsapi.DNS_ERROR_RCODE_FORMAT_ERROR => DnsResponseCode.FormatError, - Interop.Dnsapi.DNS_ERROR_RCODE_SERVER_FAILURE => DnsResponseCode.ServerFailure, - Interop.Dnsapi.DNS_ERROR_RCODE_NOT_IMPLEMENTED => DnsResponseCode.NotImplemented, - Interop.Dnsapi.DNS_ERROR_RCODE_REFUSED => DnsResponseCode.Refused, - _ => DnsResponseCode.ServerFailure, - }; - - private static TimeSpan ExtractNegativeCacheTtl(IntPtr head) - { - // Walk the record list looking for an SOA in the authority section. - for (IntPtr cur = head; cur != IntPtr.Zero; ) - { - Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); - uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; - if (hdr.wType == Interop.Dnsapi.DNS_TYPE_SOA && section == Interop.Dnsapi.DNSREC_AUTHORITY) - { - IntPtr dataPtr = cur + Marshal.SizeOf(); - Interop.Dnsapi.DNS_SOA_DATA soa = Marshal.PtrToStructure(dataPtr); - // RFC 2308 §5: negative cache TTL = min(SOA TTL, SOA MINIMUM) - uint negTtl = Math.Min(hdr.dwTtl, soa.dwDefaultTtl); - return TimeSpan.FromSeconds(negTtl); - } - cur = hdr.pNext; - } - return TimeSpan.Zero; - } - } -} diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs index 29da9b9260d38c..1bf3a0521690fa 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs @@ -161,6 +161,36 @@ public ValueTask DisposeAsync() return ValueTask.CompletedTask; } + // ---- Resolve*Core methods ---- + // + // These instance methods are the platform-agnostic seam between the public + // API and the platform abstraction layer (DnsResolverPal). They exist as a + // dedicated layer so that instrumentation and telemetry can be added here + // later without touching either the public surface or the PAL. The `async` + // flag is threaded down to the PAL, which issues the underlying query + // synchronously or asynchronously accordingly. + + private Task> ResolveAddressesCore(bool async, string name, AddressFamily addressFamily, CancellationToken cancellationToken) + => DnsResolverPal.ResolveAddresses(_options.Servers, async, name, addressFamily, cancellationToken); + + private Task> ResolveSrvCore(bool async, string name, CancellationToken cancellationToken) + => DnsResolverPal.ResolveSrv(_options.Servers, async, name, cancellationToken); + + private Task> ResolveMxCore(bool async, string name, CancellationToken cancellationToken) + => DnsResolverPal.ResolveMx(_options.Servers, async, name, cancellationToken); + + private Task> ResolveTxtCore(bool async, string name, CancellationToken cancellationToken) + => DnsResolverPal.ResolveTxt(_options.Servers, async, name, cancellationToken); + + private Task> ResolveCNameCore(bool async, string name, CancellationToken cancellationToken) + => DnsResolverPal.ResolveCName(_options.Servers, async, name, cancellationToken); + + private Task> ResolvePtrCore(bool async, string name, CancellationToken cancellationToken) + => DnsResolverPal.ResolvePtr(_options.Servers, async, name, cancellationToken); + + private Task> ResolveNsCore(bool async, string name, CancellationToken cancellationToken) + => DnsResolverPal.ResolveNs(_options.Servers, async, name, cancellationToken); + private static void ValidateName(string name) { ArgumentException.ThrowIfNullOrEmpty(name); diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Unsupported.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Unsupported.cs new file mode 100644 index 00000000000000..932a91b4822d0f --- /dev/null +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Unsupported.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net +{ + internal static partial class DnsResolverPal + { + public static Task> ResolveAddresses(IList servers, bool async, string name, AddressFamily addressFamily, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + + public static Task> ResolveSrv(IList servers, bool async, string name, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + + public static Task> ResolveMx(IList servers, bool async, string name, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + + public static Task> ResolveTxt(IList servers, bool async, string name, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + + public static Task> ResolveCName(IList servers, bool async, string name, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + + public static Task> ResolvePtr(IList servers, bool async, string name, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + + public static Task> ResolveNs(IList servers, bool async, string name, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + } +} diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs new file mode 100644 index 00000000000000..16264e8ddd314c --- /dev/null +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs @@ -0,0 +1,801 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net +{ + // Windows DNS resolver implementation backed by the Win32 DnsQueryEx API. + internal static partial class DnsResolverPal + { + // ---- Public PAL entry points (one per record type) ---- + // + // Each method takes a `bool async` flag controlling whether the underlying + // DnsQueryEx call is issued asynchronously (via the completion-callback state + // machine) or synchronously (inline on the calling thread). When async is + // false the returned Task is already completed, so the synchronous public + // entry points can safely unwrap it without blocking a thread. + + public static async Task> ResolveAddresses(IList servers, bool async, string name, AddressFamily addressFamily, CancellationToken cancellationToken) + { + if (addressFamily == AddressFamily.Unspecified) + { + if (async) + { + // Issue A and AAAA in parallel; merge results. + Task> aTask = QueryAddresses(servers, async: true, name, Interop.Dnsapi.DNS_TYPE_A, cancellationToken); + Task> aaaaTask = QueryAddresses(servers, async: true, name, Interop.Dnsapi.DNS_TYPE_AAAA, cancellationToken); + DnsResult aRes = await aTask.ConfigureAwait(false); + DnsResult aaaaRes = await aaaaTask.ConfigureAwait(false); + return MergeAddressResults(aRes, aaaaRes); + } + else + { + // Synchronous: query A then AAAA sequentially. + DnsResult aRes = await QueryAddresses(servers, async: false, name, Interop.Dnsapi.DNS_TYPE_A, cancellationToken).ConfigureAwait(false); + DnsResult aaaaRes = await QueryAddresses(servers, async: false, name, Interop.Dnsapi.DNS_TYPE_AAAA, cancellationToken).ConfigureAwait(false); + return MergeAddressResults(aRes, aaaaRes); + } + } + + ushort qtype = AddressFamilyToQueryType(addressFamily); + return await QueryAddresses(servers, async, name, qtype, cancellationToken).ConfigureAwait(false); + } + + public static async Task> ResolveSrv(IList servers, bool async, string name, CancellationToken cancellationToken) + { + DnsQueryRawResult raw = await DnsQueryEx(servers, async, name, Interop.Dnsapi.DNS_TYPE_SRV, cancellationToken).ConfigureAwait(false); + try + { + return ParseSrv(raw); + } + finally + { + raw.Dispose(); + } + } + + public static Task> ResolveMx(IList servers, bool async, string name, CancellationToken cancellationToken) + => QuerySimple(servers, async, name, Interop.Dnsapi.DNS_TYPE_MX, cancellationToken, s_parseMx); + + public static Task> ResolveCName(IList servers, bool async, string name, CancellationToken cancellationToken) + => QuerySimple(servers, async, name, Interop.Dnsapi.DNS_TYPE_CNAME, cancellationToken, s_parseCName); + + public static Task> ResolvePtr(IList servers, bool async, string name, CancellationToken cancellationToken) + => QuerySimple(servers, async, name, Interop.Dnsapi.DNS_TYPE_PTR, cancellationToken, s_parsePtr); + + public static Task> ResolveNs(IList servers, bool async, string name, CancellationToken cancellationToken) + => QuerySimple(servers, async, name, Interop.Dnsapi.DNS_TYPE_NS, cancellationToken, s_parseNs); + + public static async Task> ResolveTxt(IList servers, bool async, string name, CancellationToken cancellationToken) + { + DnsQueryRawResult raw = await DnsQueryEx(servers, async, name, Interop.Dnsapi.DNS_TYPE_TEXT, cancellationToken).ConfigureAwait(false); + try + { + return ParseTxt(raw); + } + finally + { + raw.Dispose(); + } + } + + // ---- Per-record-type selectors (shared by all record types) ---- + + private static readonly Func s_parseMx = static (hdr, dataPtr) => + { + Interop.Dnsapi.DNS_MX_DATA data = Marshal.PtrToStructure(dataPtr); + return new MxRecord(PtrToString(data.pNameExchange) ?? string.Empty, data.wPreference, TimeSpan.FromSeconds(hdr.dwTtl)); + }; + + private static readonly Func s_parseCName = static (hdr, dataPtr) => + { + Interop.Dnsapi.DNS_CNAME_DATA data = Marshal.PtrToStructure(dataPtr); + return new CNameRecord(PtrToString(data.pNameHost) ?? string.Empty, TimeSpan.FromSeconds(hdr.dwTtl)); + }; + + private static readonly Func s_parsePtr = static (hdr, dataPtr) => + { + Interop.Dnsapi.DNS_PTR_DATA data = Marshal.PtrToStructure(dataPtr); + return new PtrRecord(PtrToString(data.pNameHost) ?? string.Empty, TimeSpan.FromSeconds(hdr.dwTtl)); + }; + + private static readonly Func s_parseNs = static (hdr, dataPtr) => + { + Interop.Dnsapi.DNS_NS_DATA data = Marshal.PtrToStructure(dataPtr); + return new NsRecord(PtrToString(data.pNameHost) ?? string.Empty, TimeSpan.FromSeconds(hdr.dwTtl)); + }; + + private static ushort AddressFamilyToQueryType(AddressFamily addressFamily) => + addressFamily switch + { + AddressFamily.InterNetwork => Interop.Dnsapi.DNS_TYPE_A, + AddressFamily.InterNetworkV6 => Interop.Dnsapi.DNS_TYPE_AAAA, + _ => throw new ArgumentException(SR.net_invalid_ip_addr, nameof(addressFamily)), + }; + + // ---- Query wrappers (issue the query, then parse the record list) ---- + + private static async Task> QueryAddresses(IList servers, bool async, string name, ushort qtype, CancellationToken cancellationToken) + { + DnsQueryRawResult raw = await DnsQueryEx(servers, async, name, qtype, cancellationToken).ConfigureAwait(false); + try + { + return ParseAddresses(raw, qtype); + } + finally + { + raw.Dispose(); + } + } + + private static async Task> QuerySimple(IList servers, bool async, string name, ushort qtype, CancellationToken cancellationToken, + Func selector) + { + DnsQueryRawResult raw = await DnsQueryEx(servers, async, name, qtype, cancellationToken).ConfigureAwait(false); + try + { + return ParseSimple(raw, qtype, selector); + } + finally + { + raw.Dispose(); + } + } + + // ---- Record-list parsers ---- + + private static DnsResult ParseAddresses(DnsQueryRawResult raw, ushort qtype) + { + if (raw.ResponseCode != DnsResponseCode.NoError) + { + return new DnsResult(raw.ResponseCode, null, raw.NegativeCacheTtl); + } + + List records = new(); + for (IntPtr cur = raw.RecordsHead; cur != IntPtr.Zero; ) + { + Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); + uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; + if (hdr.wType == qtype && section == Interop.Dnsapi.DNSREC_ANSWER) + { + IntPtr dataPtr = cur + Marshal.SizeOf(); + if (TryParseAddress(hdr.wType, dataPtr, out IPAddress? address)) + { + records.Add(new AddressRecord(address!, TimeSpan.FromSeconds(hdr.dwTtl))); + } + } + cur = hdr.pNext; + } + + return new DnsResult(DnsResponseCode.NoError, records, TimeSpan.Zero); + } + + private static DnsResult ParseSrv(DnsQueryRawResult raw) + { + if (raw.ResponseCode != DnsResponseCode.NoError) + { + return new DnsResult(raw.ResponseCode, null, raw.NegativeCacheTtl); + } + + // Gather additional-section A/AAAA records by name so we can attach them. + Dictionary>? glue = null; + ParseAdditionalAddresses(raw.RecordsHead, ref glue); + + List records = new(); + for (IntPtr cur = raw.RecordsHead; cur != IntPtr.Zero; ) + { + Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); + uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; + if (hdr.wType == Interop.Dnsapi.DNS_TYPE_SRV && section == Interop.Dnsapi.DNSREC_ANSWER) + { + IntPtr dataPtr = cur + Marshal.SizeOf(); + Interop.Dnsapi.DNS_SRV_DATA data = Marshal.PtrToStructure(dataPtr); + string target = PtrToString(data.pNameTarget) ?? string.Empty; + IReadOnlyList? attached = null; + if (glue != null && glue.TryGetValue(target, out List? list)) + { + attached = list; + } + records.Add(new SrvRecord(target, data.wPort, data.wPriority, data.wWeight, TimeSpan.FromSeconds(hdr.dwTtl), attached)); + } + cur = hdr.pNext; + } + + return new DnsResult(DnsResponseCode.NoError, records, TimeSpan.Zero); + } + + private static DnsResult ParseTxt(DnsQueryRawResult raw) + { + if (raw.ResponseCode != DnsResponseCode.NoError) + { + return new DnsResult(raw.ResponseCode, null, raw.NegativeCacheTtl); + } + + List records = new(); + for (IntPtr cur = raw.RecordsHead; cur != IntPtr.Zero; ) + { + Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); + uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; + if (hdr.wType == Interop.Dnsapi.DNS_TYPE_TEXT && section == Interop.Dnsapi.DNSREC_ANSWER) + { + IntPtr dataPtr = cur + Marshal.SizeOf(); + // DNS_TXT_DATA: uint dwStringCount; followed by array of PCWSTR. + uint count = (uint)Marshal.ReadInt32(dataPtr); + int ptrSize = IntPtr.Size; + IntPtr stringsPtr = dataPtr + sizeof(uint); + if (ptrSize > sizeof(uint)) + { + // Round up to pointer alignment. + long aligned = ((long)stringsPtr + (ptrSize - 1)) & ~(long)(ptrSize - 1); + stringsPtr = checked((nint)aligned); + } + string[] values = new string[count]; + for (int i = 0; i < count; i++) + { + IntPtr strPtr = Marshal.ReadIntPtr(stringsPtr, i * ptrSize); + values[i] = PtrToString(strPtr) ?? string.Empty; + } + records.Add(new TxtRecord(values, TimeSpan.FromSeconds(hdr.dwTtl))); + } + cur = hdr.pNext; + } + + return new DnsResult(DnsResponseCode.NoError, records, TimeSpan.Zero); + } + + private static DnsResult ParseSimple(DnsQueryRawResult raw, ushort qtype, + Func selector) + { + if (raw.ResponseCode != DnsResponseCode.NoError) + { + return new DnsResult(raw.ResponseCode, null, raw.NegativeCacheTtl); + } + + List records = new(); + for (IntPtr cur = raw.RecordsHead; cur != IntPtr.Zero; ) + { + Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); + uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; + if (hdr.wType == qtype && section == Interop.Dnsapi.DNSREC_ANSWER) + { + IntPtr dataPtr = cur + Marshal.SizeOf(); + records.Add(selector(hdr, dataPtr)); + } + cur = hdr.pNext; + } + + return new DnsResult(DnsResponseCode.NoError, records, TimeSpan.Zero); + } + + private static DnsResult MergeAddressResults(DnsResult a, DnsResult b) + { + if (a.Records.Count > 0 || b.Records.Count > 0) + { + AddressRecord[] merged = new AddressRecord[a.Records.Count + b.Records.Count]; + int idx = 0; + for (int i = 0; i < a.Records.Count; i++) merged[idx++] = a.Records[i]; + for (int i = 0; i < b.Records.Count; i++) merged[idx++] = b.Records[i]; + return new DnsResult(DnsResponseCode.NoError, merged, TimeSpan.Zero); + } + + DnsResponseCode chosenRc = a.ResponseCode == DnsResponseCode.NxDomain || b.ResponseCode == DnsResponseCode.NxDomain + ? DnsResponseCode.NxDomain + : (a.ResponseCode != DnsResponseCode.NoError ? a.ResponseCode : b.ResponseCode); + TimeSpan negTtl = a.NegativeCacheTtl > TimeSpan.Zero ? a.NegativeCacheTtl : b.NegativeCacheTtl; + return new DnsResult(chosenRc, null, negTtl); + } + + private static bool TryParseAddress(ushort recordType, IntPtr dataPtr, out IPAddress? address) + { + if (recordType == Interop.Dnsapi.DNS_TYPE_A) + { + uint ip = (uint)Marshal.ReadInt32(dataPtr); + address = new IPAddress(ip); + return true; + } + if (recordType == Interop.Dnsapi.DNS_TYPE_AAAA) + { + byte[] bytes = new byte[16]; + Marshal.Copy(dataPtr, bytes, 0, 16); + address = new IPAddress(bytes); + return true; + } + address = null; + return false; + } + + private static void ParseAdditionalAddresses(IntPtr head, ref Dictionary>? glue) + { + for (IntPtr cur = head; cur != IntPtr.Zero; ) + { + Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); + uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; + bool isAddress = hdr.wType == Interop.Dnsapi.DNS_TYPE_A || hdr.wType == Interop.Dnsapi.DNS_TYPE_AAAA; + if (section == Interop.Dnsapi.DNSREC_ADDITIONAL && isAddress) + { + IntPtr dataPtr = cur + Marshal.SizeOf(); + if (TryParseAddress(hdr.wType, dataPtr, out IPAddress? address)) + { + string name = PtrToString(hdr.pName) ?? string.Empty; + glue ??= new Dictionary>(StringComparer.OrdinalIgnoreCase); + if (!glue.TryGetValue(name, out List? list)) + { + list = new List(); + glue[name] = list; + } + list.Add(new AddressRecord(address!, TimeSpan.FromSeconds(hdr.dwTtl))); + } + } + cur = hdr.pNext; + } + } + + // ---- Core DnsQueryEx wrapper ---- + + private static Task DnsQueryEx(IList servers, bool async, string name, ushort queryType, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + if (async) + { + DnsQueryAsyncState state = new DnsQueryAsyncState(servers, name, queryType, cancellationToken); + return state.StartAsync(); + } + + // Synchronous: the result is produced inline, so the returned Task is + // already completed and the sync entry points unwrap it without blocking. + return Task.FromResult(DnsQueryExSync(servers, name, queryType, cancellationToken)); + } + + private static unsafe string? PtrToString(IntPtr p) => + p == IntPtr.Zero ? null : Marshal.PtrToStringUni(p); + + // ---- Asynchronous DnsQueryEx state machine ---- + + // Cached callback so we don't allocate a new delegate per query. + private static readonly Interop.Dnsapi.DnsQueryCompletionRoutine s_completionCallback = QueryCompletionCallback; + private static readonly IntPtr s_completionCallbackPtr = + Marshal.GetFunctionPointerForDelegate(s_completionCallback); + + /// + /// Holds the unmanaged state for a single DnsQueryEx invocation, including + /// the request/result/cancel structures, the pinned query name, and the + /// completion TaskCompletionSource. + /// + private sealed unsafe class DnsQueryAsyncState + { + private readonly TaskCompletionSource _tcs = + new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly string _name; + private readonly ushort _queryType; + private readonly CancellationToken _cancellationToken; + private readonly IList _servers; + + private GCHandle _selfHandle; + private IntPtr _namePtr; + private IntPtr _requestPtr; + private IntPtr _resultPtr; + private IntPtr _cancelPtr; + private IntPtr _serverListPtr; // DNS_ADDR_ARRAY buffer + private CancellationTokenRegistration _ctReg; + private int _completed; // 0 = pending, 1 = completed (callback or sync) + + public DnsQueryAsyncState(IList servers, string name, ushort queryType, CancellationToken cancellationToken) + { + _servers = servers; + _name = name; + _queryType = queryType; + _cancellationToken = cancellationToken; + } + + public Task StartAsync() + { + ValidateServerPorts(_servers); + + try + { + _namePtr = Marshal.StringToHGlobalUni(_name); + _resultPtr = Marshal.AllocHGlobal(sizeof(Interop.Dnsapi.DNS_QUERY_RESULT)); + NativeMemory.Clear((void*)_resultPtr, (nuint)sizeof(Interop.Dnsapi.DNS_QUERY_RESULT)); + Interop.Dnsapi.DNS_QUERY_RESULT* result = (Interop.Dnsapi.DNS_QUERY_RESULT*)_resultPtr; + result->Version = Interop.Dnsapi.DNS_QUERY_REQUEST_VERSION1; + + _cancelPtr = Marshal.AllocHGlobal(sizeof(Interop.Dnsapi.DNS_QUERY_CANCEL)); + NativeMemory.Clear((void*)_cancelPtr, (nuint)sizeof(Interop.Dnsapi.DNS_QUERY_CANCEL)); + + _selfHandle = GCHandle.Alloc(this); + + _requestPtr = Marshal.AllocHGlobal(sizeof(Interop.Dnsapi.DNS_QUERY_REQUEST)); + NativeMemory.Clear((void*)_requestPtr, (nuint)sizeof(Interop.Dnsapi.DNS_QUERY_REQUEST)); + Interop.Dnsapi.DNS_QUERY_REQUEST* req = (Interop.Dnsapi.DNS_QUERY_REQUEST*)_requestPtr; + req->Version = Interop.Dnsapi.DNS_QUERY_REQUEST_VERSION1; + req->QueryName = _namePtr; + req->QueryType = _queryType; + req->QueryOptions = Interop.Dnsapi.DNS_QUERY_STANDARD; + req->InterfaceIndex = 0; + req->pQueryCompletionCallback = s_completionCallbackPtr; + req->pQueryContext = GCHandle.ToIntPtr(_selfHandle); + + if (_servers is { Count: > 0 }) + { + BuildAddrArray(_servers, out _serverListPtr); + req->pDnsServerList = (Interop.Dnsapi.DNS_ADDR_ARRAY*)_serverListPtr; + } + + int status = Interop.Dnsapi.DnsQueryEx( + (Interop.Dnsapi.DNS_QUERY_REQUEST*)_requestPtr, + (Interop.Dnsapi.DNS_QUERY_RESULT*)_resultPtr, + (Interop.Dnsapi.DNS_QUERY_CANCEL*)_cancelPtr); + + if (status == Interop.Dnsapi.DNS_REQUEST_PENDING) + { + // Async. Register cancellation; the callback will free resources and complete the TCS. + if (_cancellationToken.CanBeCanceled) + { + _ctReg = _cancellationToken.UnsafeRegister(static (s, _) => + { + DnsQueryAsyncState st = (DnsQueryAsyncState)s!; + st.CancelAndAbort(); + }, this); + } + } + else + { + // Synchronous completion. The callback was NOT invoked; we complete inline. + CompleteFromResult(status); + } + } + catch + { + FreeAll(); + throw; + } + + return _tcs.Task; + } + + private void CancelAndAbort() + { + if (_cancelPtr != IntPtr.Zero) + { + Interop.Dnsapi.DnsCancelQuery((Interop.Dnsapi.DNS_QUERY_CANCEL*)_cancelPtr); + } + } + + /// + /// Invoked from either the native callback or the sync completion path. + /// Parses the QueryStatus and pQueryRecords from the result struct, + /// completes the TCS, and frees state. + /// + internal void CompleteFromResult(int status) + { + if (Interlocked.Exchange(ref _completed, 1) != 0) + { + return; + } + + try + { + _ctReg.Dispose(); + + Interop.Dnsapi.DNS_QUERY_RESULT result = Marshal.PtrToStructure(_resultPtr); + IntPtr records = result.pQueryRecords; + + if (_cancellationToken.IsCancellationRequested) + { + if (records != IntPtr.Zero) + { + Interop.Dnsapi.DnsFree(records, Interop.Dnsapi.DnsFreeRecordList); + } + _tcs.TrySetCanceled(_cancellationToken); + return; + } + + DnsResponseCode rc = MapWindowsErrorToResponseCode(status); + + // 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); + } + + _tcs.TrySetResult(new DnsQueryRawResult(rc, records, negativeTtl)); + } + catch (Exception ex) + { + _tcs.TrySetException(ex); + } + finally + { + FreeAll(); + } + } + + private void FreeAll() + { + if (_namePtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(_namePtr); + _namePtr = IntPtr.Zero; + } + if (_requestPtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(_requestPtr); + _requestPtr = IntPtr.Zero; + } + if (_resultPtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(_resultPtr); + _resultPtr = IntPtr.Zero; + } + if (_cancelPtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(_cancelPtr); + _cancelPtr = IntPtr.Zero; + } + if (_serverListPtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(_serverListPtr); + _serverListPtr = IntPtr.Zero; + } + if (_selfHandle.IsAllocated) + { + _selfHandle.Free(); + } + } + } + + // Native callback. Marshaled to a function pointer once at startup. + // We use a managed delegate (no UnmanagedCallersOnly) because callers + // currently pass it via Marshal.GetFunctionPointerForDelegate. + private static void QueryCompletionCallback(IntPtr pQueryContext, IntPtr pQueryResults) + { + try + { + GCHandle handle = GCHandle.FromIntPtr(pQueryContext); + DnsQueryAsyncState? state = handle.Target as DnsQueryAsyncState; + if (state == null) + { + return; + } + + // pQueryResults points to the same DNS_QUERY_RESULT we passed in. + unsafe + { + Interop.Dnsapi.DNS_QUERY_RESULT* res = (Interop.Dnsapi.DNS_QUERY_RESULT*)pQueryResults; + state.CompleteFromResult(res->QueryStatus); + } + } + catch + { + // Swallow — never allow exceptions to propagate into native code. + } + } + + // DnsQueryEx only supports DNS servers reachable on the standard port 53. + // The sockaddr port field passed to the API must be 0 (the API always + // queries port 53); supplying any non-zero port - even 53 itself - results + // in ERROR_INVALID_PARAMETER. We therefore reject any server endpoint that + // requests a non-default port, since it cannot be honored on Windows. + private static void ValidateServerPorts(IList servers) + { + if (servers is { Count: > 0 }) + { + foreach (IPEndPoint ep in servers) + { + if (ep.Port != 0 && ep.Port != 53) + { + throw new PlatformNotSupportedException(SR.net_dns_custom_port_not_supported); + } + } + } + } + + // Synchronous DnsQueryEx invocation. By omitting the completion callback the + // API executes the query inline on the calling thread and returns the result + // directly, so no GCHandle / TaskCompletionSource bookkeeping is required. + private static unsafe DnsQueryRawResult DnsQueryExSync(IList servers, string name, ushort queryType, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + ValidateServerPorts(servers); + + IntPtr namePtr = IntPtr.Zero; + IntPtr serverListPtr = IntPtr.Zero; + try + { + namePtr = Marshal.StringToHGlobalUni(name); + + Interop.Dnsapi.DNS_QUERY_RESULT result = default; + result.Version = Interop.Dnsapi.DNS_QUERY_REQUEST_VERSION1; + + Interop.Dnsapi.DNS_QUERY_REQUEST request = default; + request.Version = Interop.Dnsapi.DNS_QUERY_REQUEST_VERSION1; + request.QueryName = namePtr; + request.QueryType = queryType; + request.QueryOptions = Interop.Dnsapi.DNS_QUERY_STANDARD; + // No completion callback => synchronous execution. + + if (servers is { Count: > 0 }) + { + BuildAddrArray(servers, out serverListPtr); + request.pDnsServerList = (Interop.Dnsapi.DNS_ADDR_ARRAY*)serverListPtr; + } + + // A null cancel handle is valid for synchronous queries. + int status = Interop.Dnsapi.DnsQueryEx(&request, &result, null); + + IntPtr records = result.pQueryRecords; + + if (cancellationToken.IsCancellationRequested) + { + if (records != IntPtr.Zero) + { + Interop.Dnsapi.DnsFree(records, Interop.Dnsapi.DnsFreeRecordList); + } + throw new OperationCanceledException(cancellationToken); + } + + DnsResponseCode rc = MapWindowsErrorToResponseCode(status); + + // 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); + } + + return new DnsQueryRawResult(rc, records, negativeTtl); + } + finally + { + if (namePtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(namePtr); + } + if (serverListPtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(serverListPtr); + } + } + } + + private static unsafe void BuildAddrArray(IList servers, out IntPtr arrayPtr) + { + int count = servers.Count; + int headerSize = sizeof(Interop.Dnsapi.DNS_ADDR_ARRAY); + int addrSize = sizeof(Interop.Dnsapi.DNS_ADDR); + int totalSize = headerSize + addrSize * count; + + arrayPtr = Marshal.AllocHGlobal(totalSize); + NativeMemory.Clear((void*)arrayPtr, (nuint)totalSize); + + Interop.Dnsapi.DNS_ADDR_ARRAY* arr = (Interop.Dnsapi.DNS_ADDR_ARRAY*)arrayPtr; + arr->MaxCount = (uint)count; + arr->AddrCount = (uint)count; + arr->Family = (ushort)(servers[0].AddressFamily == AddressFamily.InterNetwork ? Interop.Dnsapi.AF_INET : Interop.Dnsapi.AF_INET6); + + byte* addrBase = (byte*)arrayPtr + headerSize; + for (int i = 0; i < count; i++) + { + IPEndPoint ep = servers[i]; + byte* sa = addrBase + (i * addrSize); + WriteSockAddr(sa, ep); + } + } + + // 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) + { + // DnsQueryEx always queries DNS servers on port 53 and requires the sockaddr + // port field to be left as 0. Supplying a non-zero port (even 53) is rejected + // with ERROR_INVALID_PARAMETER. Non-default ports are validated and rejected + // earlier, so the port is always written as 0 here. + if (ep.AddressFamily == AddressFamily.InterNetwork) + { + // sockaddr_in: ushort family, ushort port (net order), uint addr, 8 bytes zero + *(ushort*)(dest + 0) = Interop.Dnsapi.AF_INET; + // dest[2..3] (port) left zero + Span 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]; + // dest[8..15] left zero + } + else if (ep.AddressFamily == AddressFamily.InterNetworkV6) + { + // sockaddr_in6: ushort family, ushort port, uint flowinfo, 16-byte addr, uint scope_id + *(ushort*)(dest + 0) = Interop.Dnsapi.AF_INET6; + // dest[2..3] (port) left zero + // flowinfo (dest[4..7]) left zero + Span addrBytes = stackalloc byte[16]; + ep.Address.TryWriteBytes(addrBytes, out _); + for (int i = 0; i < 16; i++) + { + dest[8 + i] = addrBytes[i]; + } + // scope_id (dest[24..27]) + uint scopeId = (uint)ep.Address.ScopeId; + dest[24] = (byte)(scopeId & 0xff); + dest[25] = (byte)((scopeId >> 8) & 0xff); + dest[26] = (byte)((scopeId >> 16) & 0xff); + dest[27] = (byte)((scopeId >> 24) & 0xff); + } + else + { + throw new ArgumentException(SR.net_invalid_ip_addr); + } + } + + private static DnsResponseCode MapWindowsErrorToResponseCode(int status) => + status switch + { + Interop.Dnsapi.ERROR_SUCCESS => DnsResponseCode.NoError, + Interop.Dnsapi.DNS_INFO_NO_RECORDS => DnsResponseCode.NoError, // NODATA: name exists but no records of requested type + Interop.Dnsapi.DNS_ERROR_RCODE_NAME_ERROR => DnsResponseCode.NxDomain, + Interop.Dnsapi.DNS_ERROR_RCODE_FORMAT_ERROR => DnsResponseCode.FormatError, + Interop.Dnsapi.DNS_ERROR_RCODE_SERVER_FAILURE => DnsResponseCode.ServerFailure, + Interop.Dnsapi.DNS_ERROR_RCODE_NOT_IMPLEMENTED => DnsResponseCode.NotImplemented, + Interop.Dnsapi.DNS_ERROR_RCODE_REFUSED => DnsResponseCode.Refused, + _ => DnsResponseCode.ServerFailure, + }; + + private static TimeSpan ExtractNegativeCacheTtl(IntPtr head) + { + // Walk the record list looking for an SOA in the authority section. + for (IntPtr cur = head; cur != IntPtr.Zero; ) + { + Interop.Dnsapi.DNS_RECORD_HEADER hdr = Marshal.PtrToStructure(cur); + uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; + if (hdr.wType == Interop.Dnsapi.DNS_TYPE_SOA && section == Interop.Dnsapi.DNSREC_AUTHORITY) + { + IntPtr dataPtr = cur + Marshal.SizeOf(); + Interop.Dnsapi.DNS_SOA_DATA soa = Marshal.PtrToStructure(dataPtr); + // RFC 2308 §5: negative cache TTL = min(SOA TTL, SOA MINIMUM) + uint negTtl = Math.Min(hdr.dwTtl, soa.dwDefaultTtl); + return TimeSpan.FromSeconds(negTtl); + } + cur = hdr.pNext; + } + return TimeSpan.Zero; + } + + // ---- Raw query result returned by the low-level helpers ---- + + private readonly struct DnsQueryRawResult : IDisposable + { + public DnsResponseCode ResponseCode { get; } + public IntPtr RecordsHead { get; } + public TimeSpan NegativeCacheTtl { get; } + + public DnsQueryRawResult(DnsResponseCode responseCode, IntPtr recordsHead, TimeSpan negativeCacheTtl) + { + ResponseCode = responseCode; + RecordsHead = recordsHead; + NegativeCacheTtl = negativeCacheTtl; + } + + public void Dispose() + { + if (RecordsHead != IntPtr.Zero) + { + Interop.Dnsapi.DnsFree(RecordsHead, Interop.Dnsapi.DnsFreeRecordList); + } + } + } + } +} From 6157140048caf5425516c27e4bd70cef64236cb3 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Thu, 11 Jun 2026 18:02:48 +0200 Subject: [PATCH 09/19] Add telemetry to DnsResolver 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> --- .../src/System/Net/DnsResolver.cs | 95 ++++++++++++++++--- .../src/System/Net/NameResolutionTelemetry.cs | 1 + 2 files changed, 84 insertions(+), 12 deletions(-) diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs index 1bf3a0521690fa..3e91681887ee8d 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.Diagnostics; using System.Net.Sockets; using System.Threading; @@ -164,32 +165,102 @@ public ValueTask DisposeAsync() // ---- Resolve*Core methods ---- // // These instance methods are the platform-agnostic seam between the public - // API and the platform abstraction layer (DnsResolverPal). They exist as a - // dedicated layer so that instrumentation and telemetry can be added here - // later without touching either the public surface or the PAL. The `async` - // flag is threaded down to the PAL, which issues the underlying query - // synchronously or asynchronously accordingly. + // API and the platform abstraction layer (DnsResolverPal). They issue the + // underlying query through the PAL (synchronously or asynchronously per the + // `async` flag) and wrap it with telemetry. When no diagnostics consumer is + // enabled, the PAL task is returned directly so the common path stays + // allocation-free and, on the synchronous path, completes inline. private Task> ResolveAddressesCore(bool async, string name, AddressFamily addressFamily, CancellationToken cancellationToken) - => DnsResolverPal.ResolveAddresses(_options.Servers, async, name, addressFamily, cancellationToken); + { + Task> task = DnsResolverPal.ResolveAddresses(_options.Servers, async, name, addressFamily, cancellationToken); + return NameResolutionTelemetry.AnyDiagnosticsEnabled() + ? ResolveWithTelemetry(name, task, static r => MapAnswers(r, static a => a.Address.ToString())) + : task; + } private Task> ResolveSrvCore(bool async, string name, CancellationToken cancellationToken) - => DnsResolverPal.ResolveSrv(_options.Servers, async, name, cancellationToken); + { + Task> task = DnsResolverPal.ResolveSrv(_options.Servers, async, name, cancellationToken); + return NameResolutionTelemetry.AnyDiagnosticsEnabled() + ? ResolveWithTelemetry(name, task, static r => MapAnswers(r, static a => a.Target)) + : task; + } private Task> ResolveMxCore(bool async, string name, CancellationToken cancellationToken) - => DnsResolverPal.ResolveMx(_options.Servers, async, name, cancellationToken); + { + Task> task = DnsResolverPal.ResolveMx(_options.Servers, async, name, cancellationToken); + return NameResolutionTelemetry.AnyDiagnosticsEnabled() + ? ResolveWithTelemetry(name, task, static r => MapAnswers(r, static a => a.Exchange)) + : task; + } private Task> ResolveTxtCore(bool async, string name, CancellationToken cancellationToken) - => DnsResolverPal.ResolveTxt(_options.Servers, async, name, cancellationToken); + { + Task> task = DnsResolverPal.ResolveTxt(_options.Servers, async, name, cancellationToken); + return NameResolutionTelemetry.AnyDiagnosticsEnabled() + ? ResolveWithTelemetry(name, task, static r => + { + List values = new(); + foreach (TxtRecord record in r.Records) + { + values.AddRange(record.Values); + } + return values.ToArray(); + }) + : task; + } private Task> ResolveCNameCore(bool async, string name, CancellationToken cancellationToken) - => DnsResolverPal.ResolveCName(_options.Servers, async, name, cancellationToken); + { + Task> task = DnsResolverPal.ResolveCName(_options.Servers, async, name, cancellationToken); + return NameResolutionTelemetry.AnyDiagnosticsEnabled() + ? ResolveWithTelemetry(name, task, static r => MapAnswers(r, static a => a.CanonicalName)) + : task; + } private Task> ResolvePtrCore(bool async, string name, CancellationToken cancellationToken) - => DnsResolverPal.ResolvePtr(_options.Servers, async, name, cancellationToken); + { + Task> task = DnsResolverPal.ResolvePtr(_options.Servers, async, name, cancellationToken); + return NameResolutionTelemetry.AnyDiagnosticsEnabled() + ? ResolveWithTelemetry(name, task, static r => MapAnswers(r, static a => a.Name)) + : task; + } private Task> ResolveNsCore(bool async, string name, CancellationToken cancellationToken) - => DnsResolverPal.ResolveNs(_options.Servers, async, name, cancellationToken); + { + Task> task = DnsResolverPal.ResolveNs(_options.Servers, async, name, cancellationToken); + return NameResolutionTelemetry.AnyDiagnosticsEnabled() + ? ResolveWithTelemetry(name, task, static r => MapAnswers(r, static a => a.Name)) + : task; + } + + private static async Task> ResolveWithTelemetry(string name, Task> queryTask, Func, string[]> getAnswers) + { + NameResolutionActivity activity = NameResolutionTelemetry.Log.BeforeResolution(name); + try + { + DnsResult result = await queryTask.ConfigureAwait(false); + NameResolutionTelemetry.Log.AfterResolution(name, in activity, getAnswers(result)); + return result; + } + catch (Exception ex) + { + NameResolutionTelemetry.Log.AfterResolution(name, in activity, answer: null, exception: ex); + throw; + } + } + + private static string[] MapAnswers(DnsResult result, Func selector) + { + IReadOnlyList records = result.Records; + string[] answers = new string[records.Count]; + for (int i = 0; i < records.Count; i++) + { + answers[i] = selector(records[i]); + } + return answers; + } private static void ValidateName(string name) { diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/NameResolutionTelemetry.cs b/src/libraries/System.Net.NameResolution/src/System/Net/NameResolutionTelemetry.cs index 59c2b47cc00a3a..1bc519b285579f 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/NameResolutionTelemetry.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/NameResolutionTelemetry.cs @@ -195,6 +195,7 @@ public bool Stop(object? answer, Exception? exception, out TimeSpan duration) string[]? answerValues = answer switch { string h => [h], + string[] values => values, IPAddress[] addresses => GetStringValues(addresses), IPHostEntry entry => GetStringValues(entry.AddressList), _ => null From 6f99d7b1c1f5ee7e46f85a135546d8f143b5b63b Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Thu, 11 Jun 2026 18:31:55 +0200 Subject: [PATCH 10/19] Fix DnsResolver telemetry under-measuring synchronous queries 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> --- .../src/System/Net/DnsResolver.cs | 72 +++++++------------ .../DnsResolverLoopbackTest.cs | 58 +++++++++++++++ 2 files changed, 85 insertions(+), 45 deletions(-) diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs index 3e91681887ee8d..483ae143f82fb4 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs @@ -169,37 +169,29 @@ public ValueTask DisposeAsync() // underlying query through the PAL (synchronously or asynchronously per the // `async` flag) and wrap it with telemetry. When no diagnostics consumer is // enabled, the PAL task is returned directly so the common path stays - // allocation-free and, on the synchronous path, completes inline. + // allocation-free and, on the synchronous path, completes inline. When + // telemetry is enabled, the PAL call is deferred into ResolveWithTelemetry so + // that the measurement starts before the query runs - on the synchronous path + // the PAL would otherwise execute the entire query before telemetry began. private Task> ResolveAddressesCore(bool async, string name, AddressFamily addressFamily, CancellationToken cancellationToken) - { - Task> task = DnsResolverPal.ResolveAddresses(_options.Servers, async, name, addressFamily, cancellationToken); - return NameResolutionTelemetry.AnyDiagnosticsEnabled() - ? ResolveWithTelemetry(name, task, static r => MapAnswers(r, static a => a.Address.ToString())) - : task; - } + => NameResolutionTelemetry.AnyDiagnosticsEnabled() + ? ResolveWithTelemetry(name, () => DnsResolverPal.ResolveAddresses(_options.Servers, async, name, addressFamily, cancellationToken), static r => MapAnswers(r, static a => a.Address.ToString())) + : DnsResolverPal.ResolveAddresses(_options.Servers, async, name, addressFamily, cancellationToken); private Task> ResolveSrvCore(bool async, string name, CancellationToken cancellationToken) - { - Task> task = DnsResolverPal.ResolveSrv(_options.Servers, async, name, cancellationToken); - return NameResolutionTelemetry.AnyDiagnosticsEnabled() - ? ResolveWithTelemetry(name, task, static r => MapAnswers(r, static a => a.Target)) - : task; - } + => NameResolutionTelemetry.AnyDiagnosticsEnabled() + ? ResolveWithTelemetry(name, () => DnsResolverPal.ResolveSrv(_options.Servers, async, name, cancellationToken), static r => MapAnswers(r, static a => a.Target)) + : DnsResolverPal.ResolveSrv(_options.Servers, async, name, cancellationToken); private Task> ResolveMxCore(bool async, string name, CancellationToken cancellationToken) - { - Task> task = DnsResolverPal.ResolveMx(_options.Servers, async, name, cancellationToken); - return NameResolutionTelemetry.AnyDiagnosticsEnabled() - ? ResolveWithTelemetry(name, task, static r => MapAnswers(r, static a => a.Exchange)) - : task; - } + => NameResolutionTelemetry.AnyDiagnosticsEnabled() + ? ResolveWithTelemetry(name, () => DnsResolverPal.ResolveMx(_options.Servers, async, name, cancellationToken), static r => MapAnswers(r, static a => a.Exchange)) + : DnsResolverPal.ResolveMx(_options.Servers, async, name, cancellationToken); private Task> ResolveTxtCore(bool async, string name, CancellationToken cancellationToken) - { - Task> task = DnsResolverPal.ResolveTxt(_options.Servers, async, name, cancellationToken); - return NameResolutionTelemetry.AnyDiagnosticsEnabled() - ? ResolveWithTelemetry(name, task, static r => + => NameResolutionTelemetry.AnyDiagnosticsEnabled() + ? ResolveWithTelemetry(name, () => DnsResolverPal.ResolveTxt(_options.Servers, async, name, cancellationToken), static r => { List values = new(); foreach (TxtRecord record in r.Records) @@ -208,39 +200,29 @@ private Task> ResolveTxtCore(bool async, string name, Cance } return values.ToArray(); }) - : task; - } + : DnsResolverPal.ResolveTxt(_options.Servers, async, name, cancellationToken); private Task> ResolveCNameCore(bool async, string name, CancellationToken cancellationToken) - { - Task> task = DnsResolverPal.ResolveCName(_options.Servers, async, name, cancellationToken); - return NameResolutionTelemetry.AnyDiagnosticsEnabled() - ? ResolveWithTelemetry(name, task, static r => MapAnswers(r, static a => a.CanonicalName)) - : task; - } + => NameResolutionTelemetry.AnyDiagnosticsEnabled() + ? ResolveWithTelemetry(name, () => DnsResolverPal.ResolveCName(_options.Servers, async, name, cancellationToken), static r => MapAnswers(r, static a => a.CanonicalName)) + : DnsResolverPal.ResolveCName(_options.Servers, async, name, cancellationToken); private Task> ResolvePtrCore(bool async, string name, CancellationToken cancellationToken) - { - Task> task = DnsResolverPal.ResolvePtr(_options.Servers, async, name, cancellationToken); - return NameResolutionTelemetry.AnyDiagnosticsEnabled() - ? ResolveWithTelemetry(name, task, static r => MapAnswers(r, static a => a.Name)) - : task; - } + => NameResolutionTelemetry.AnyDiagnosticsEnabled() + ? ResolveWithTelemetry(name, () => DnsResolverPal.ResolvePtr(_options.Servers, async, name, cancellationToken), static r => MapAnswers(r, static a => a.Name)) + : DnsResolverPal.ResolvePtr(_options.Servers, async, name, cancellationToken); private Task> ResolveNsCore(bool async, string name, CancellationToken cancellationToken) - { - Task> task = DnsResolverPal.ResolveNs(_options.Servers, async, name, cancellationToken); - return NameResolutionTelemetry.AnyDiagnosticsEnabled() - ? ResolveWithTelemetry(name, task, static r => MapAnswers(r, static a => a.Name)) - : task; - } + => NameResolutionTelemetry.AnyDiagnosticsEnabled() + ? 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> ResolveWithTelemetry(string name, Task> queryTask, Func, string[]> getAnswers) + private static async Task> ResolveWithTelemetry(string name, Func>> resolve, Func, string[]> getAnswers) { NameResolutionActivity activity = NameResolutionTelemetry.Log.BeforeResolution(name); try { - DnsResult result = await queryTask.ConfigureAwait(false); + DnsResult result = await resolve().ConfigureAwait(false); NameResolutionTelemetry.Log.AfterResolution(name, in activity, getAnswers(result)); return result; } diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs index 353df8fc6f167a..eed201f7612d1c 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Linq; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; @@ -457,5 +460,60 @@ public async Task ResolveAddresses_CancellationInFlight_Throws() serverCanContinue.Set(); } + + // ---- Telemetry ---- + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveAddresses_RecordsDurationMetric_CoversQueryTime(bool async) + { + TimeSpan delay = TimeSpan.FromMilliseconds(250); + string name = UniqueName("metrics"); + _server.AddRawResponse(name, DnsRecordType.A, queryId => + { + Thread.Sleep(delay); + return DnsResponseBuilder.For(queryId, DnsResponseBuilder.EncodeName(name), DnsRecordType.A) + .Answer(new byte[] { 10, 0, 0, 9 }, ttl: 120) + .Build(); + }); + + List> measurements = new(); + using (MeterListener listener = new()) + { + listener.InstrumentPublished = (instrument, l) => + { + if (instrument.Meter.Name == "System.Net.NameResolution" && instrument.Name == "dns.lookup.duration") + { + l.EnableMeasurementEvents(instrument); + } + }; + listener.SetMeasurementEventCallback((instrument, measurement, tags, state) => + { + lock (measurements) + { + measurements.Add(new Measurement(measurement, tags)); + } + }); + listener.Start(); + + // A single A query so exactly one lookup is measured. + DnsResult result = await ResolveAddresses(async, Resolver, name, AddressFamily.InterNetwork); + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + } + + Measurement[] matching = measurements + .Where(m => m.Tags.ToArray().Any(t => t.Key == "dns.question.name" && (string?)t.Value == name)) + .ToArray(); + + Measurement recorded = Assert.Single(matching); + + // The measured duration must span the actual query, and so must be at least + // the server's artificial response delay - the lookup cannot legitimately + // complete before the server replies. Regression: on the synchronous path + // telemetry used to start only after the PAL had already begun executing the + // query, so the recorded duration was shorter than the server delay. + Assert.True(recorded.Value >= delay.TotalSeconds, $"Expected a lookup duration of at least {delay.TotalSeconds:0.###}s but got {recorded.Value:0.###}s."); + } } } From 6068489f7864ecf02f9b2da6b74446c7902752ae Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Fri, 12 Jun 2026 13:25:16 +0200 Subject: [PATCH 11/19] Address Windows DNS PR review feedback - Conform instance DnsResolver to approved API: remove ResolvePtr(IPAddress) overloads (static Dns keeps them, delegating via BuildArpaName). - Validate DnsResolverOptions.Servers setter is non-null; resolver takes a defensive snapshot of the configured servers. - Fix negative-cache TTL: always extract from authority SOA and surface it on the success path (covers NODATA), plus merge max TTL when combining A/AAAA. - PAL cleanups: typed GCHandle, bool completion flag, span-based address parsing, mixed-address-family validation, checked size arithmetic, simplified using-based query helpers. - Allocation-free telemetry path via TState + static delegates. - Simplify reverse-arpa name building (plain interpolation / string.Concat). - Add XML docs across the new public surface; reword/extend SR strings. - Tests: assert NegativeCacheTtl for NODATA/NXDOMAIN; skip custom-server test when no system DNS server is configured; move IPAddress PTR tests to static API. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Interop/Windows/Dnsapi/Interop.DnsApi.cs | 1 - .../ref/System.Net.NameResolution.cs | 1 - .../src/Resources/Strings.resx | 5 +- .../src/System/Net/Dns.Resolve.cs | 132 ++++++++++- .../src/System/Net/DnsRecords.cs | 33 ++- .../src/System/Net/DnsResolver.cs | 222 +++++++++++++++--- .../src/System/Net/DnsResolverOptions.cs | 15 +- .../src/System/Net/DnsResolverPal.Windows.cs | 194 ++++++--------- .../src/System/Net/DnsResult.cs | 16 +- .../DnsResolverLoopbackTest.cs | 6 + .../tests/FunctionalTests/DnsResolverTest.cs | 16 +- 11 files changed, 453 insertions(+), 188 deletions(-) diff --git a/src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsApi.cs b/src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsApi.cs index ce4e396ff9a8f3..fce1cc072d8e04 100644 --- a/src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsApi.cs +++ b/src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsApi.cs @@ -3,7 +3,6 @@ using System; using System.Runtime.InteropServices; -using Microsoft.Win32.SafeHandles; internal static partial class Interop { diff --git a/src/libraries/System.Net.NameResolution/ref/System.Net.NameResolution.cs b/src/libraries/System.Net.NameResolution/ref/System.Net.NameResolution.cs index a77da0600bc3fb..ae5580a4fc268b 100644 --- a/src/libraries/System.Net.NameResolution/ref/System.Net.NameResolution.cs +++ b/src/libraries/System.Net.NameResolution/ref/System.Net.NameResolution.cs @@ -87,7 +87,6 @@ public DnsResolver(System.Net.DnsResolverOptions options) { } public System.Threading.Tasks.Task> ResolveTxtAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public System.Threading.Tasks.Task> ResolveCNameAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public System.Threading.Tasks.Task> ResolvePtrAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } - public System.Threading.Tasks.Task> ResolvePtrAsync(System.Net.IPAddress address, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public System.Threading.Tasks.Task> ResolveNsAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public void Dispose() { } public System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } diff --git a/src/libraries/System.Net.NameResolution/src/Resources/Strings.resx b/src/libraries/System.Net.NameResolution/src/Resources/Strings.resx index de6f24bdacdd75..44874623ad8cf6 100644 --- a/src/libraries/System.Net.NameResolution/src/Resources/Strings.resx +++ b/src/libraries/System.Net.NameResolution/src/Resources/Strings.resx @@ -76,6 +76,9 @@ System.Net.NameResolution is not supported on this platform. - Specifying a DNS server port other than 53 requires Windows 11 Build 22000 or later. + Specifying a custom DNS server port is not supported on this platform; only the default DNS port (53) can be used. + + + All DNS server endpoints must belong to the same address family. \ No newline at end of file diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/Dns.Resolve.cs b/src/libraries/System.Net.NameResolution/src/System/Net/Dns.Resolve.cs index eaa2b55eac439e..709265796795b6 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/Dns.Resolve.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/Dns.Resolve.cs @@ -16,60 +16,190 @@ public static partial class Dns private static DnsResolver DefaultResolver => s_defaultResolver ??= new DnsResolver(); + /// + /// Resolves the IPv4 (A) and IPv6 (AAAA) addresses for the specified host name using the system-configured DNS servers. + /// + /// The host name to resolve. + /// A containing the address records. + /// is or empty. public static DnsResult ResolveAddresses(string name) => DefaultResolver.ResolveAddresses(name); + /// + /// Resolves the addresses of the specified family for the specified host name using the system-configured DNS servers. + /// + /// The host name to resolve. + /// + /// The address family to query. Use for A records, + /// for AAAA records, or + /// for both. + /// + /// A containing the address records. + /// is or empty. public static DnsResult ResolveAddresses(string name, AddressFamily addressFamily) => DefaultResolver.ResolveAddresses(name, addressFamily); + /// + /// Asynchronously resolves the IPv4 (A) and IPv6 (AAAA) addresses for the specified host name using the system-configured DNS servers. + /// + /// The host name to resolve. + /// A token to cancel the asynchronous operation. + /// A task that completes with a containing the address records. + /// is or empty. public static Task> ResolveAddressesAsync(string name, CancellationToken cancellationToken = default) => DefaultResolver.ResolveAddressesAsync(name, cancellationToken); + /// + /// Asynchronously resolves the addresses of the specified family for the specified host name using the system-configured DNS servers. + /// + /// The host name to resolve. + /// + /// The address family to query. Use for A records, + /// for AAAA records, or + /// for both. + /// + /// A token to cancel the asynchronous operation. + /// A task that completes with a containing the address records. + /// is or empty. public static Task> ResolveAddressesAsync(string name, AddressFamily addressFamily, CancellationToken cancellationToken = default) => DefaultResolver.ResolveAddressesAsync(name, addressFamily, cancellationToken); + /// + /// Resolves the service (SRV) records for the specified name using the system-configured DNS servers. + /// + /// The name to resolve, typically in the form _service._protocol.host. + /// A containing the SRV records. + /// is or empty. public static DnsResult ResolveSrv(string name) => DefaultResolver.ResolveSrv(name); + /// + /// Asynchronously resolves the service (SRV) records for the specified name using the system-configured DNS servers. + /// + /// The name to resolve, typically in the form _service._protocol.host. + /// A token to cancel the asynchronous operation. + /// A task that completes with a containing the SRV records. + /// is or empty. public static Task> ResolveSrvAsync(string name, CancellationToken cancellationToken = default) => DefaultResolver.ResolveSrvAsync(name, cancellationToken); + /// + /// Resolves the mail exchange (MX) records for the specified name using the system-configured DNS servers. + /// + /// The domain name to resolve. + /// A containing the MX records. + /// is or empty. public static DnsResult ResolveMx(string name) => DefaultResolver.ResolveMx(name); + /// + /// Asynchronously resolves the mail exchange (MX) records for the specified name using the system-configured DNS servers. + /// + /// The domain name to resolve. + /// A token to cancel the asynchronous operation. + /// A task that completes with a containing the MX records. + /// is or empty. public static Task> ResolveMxAsync(string name, CancellationToken cancellationToken = default) => DefaultResolver.ResolveMxAsync(name, cancellationToken); + /// + /// Resolves the text (TXT) records for the specified name using the system-configured DNS servers. + /// + /// The domain name to resolve. + /// A containing the TXT records. + /// is or empty. public static DnsResult ResolveTxt(string name) => DefaultResolver.ResolveTxt(name); + /// + /// Asynchronously resolves the text (TXT) records for the specified name using the system-configured DNS servers. + /// + /// The domain name to resolve. + /// A token to cancel the asynchronous operation. + /// A task that completes with a containing the TXT records. + /// is or empty. public static Task> ResolveTxtAsync(string name, CancellationToken cancellationToken = default) => DefaultResolver.ResolveTxtAsync(name, cancellationToken); + /// + /// Resolves the canonical name (CNAME) record for the specified name using the system-configured DNS servers. + /// + /// The domain name to resolve. + /// A containing the CNAME records. + /// is or empty. public static DnsResult ResolveCName(string name) => DefaultResolver.ResolveCName(name); + /// + /// Asynchronously resolves the canonical name (CNAME) record for the specified name using the system-configured DNS servers. + /// + /// The domain name to resolve. + /// A token to cancel the asynchronous operation. + /// A task that completes with a containing the CNAME records. + /// is or empty. public static Task> ResolveCNameAsync(string name, CancellationToken cancellationToken = default) => DefaultResolver.ResolveCNameAsync(name, cancellationToken); + /// + /// Resolves the pointer (PTR) records for the specified name using the system-configured DNS servers. + /// + /// The name to resolve, typically a reverse-lookup name such as 4.3.2.1.in-addr.arpa. + /// A containing the PTR records. + /// is or empty. public static DnsResult ResolvePtr(string name) => DefaultResolver.ResolvePtr(name); + /// + /// Resolves the pointer (PTR) records for the specified IP address using the system-configured DNS servers. + /// + /// The IP address to perform a reverse lookup for. + /// A containing the PTR records. + /// is . public static DnsResult ResolvePtr(IPAddress address) { ArgumentNullException.ThrowIfNull(address); return DefaultResolver.ResolvePtr(DnsResolver.BuildArpaName(address)); } + /// + /// Asynchronously resolves the pointer (PTR) records for the specified name using the system-configured DNS servers. + /// + /// The name to resolve, typically a reverse-lookup name such as 4.3.2.1.in-addr.arpa. + /// A token to cancel the asynchronous operation. + /// A task that completes with a containing the PTR records. + /// is or empty. public static Task> ResolvePtrAsync(string name, CancellationToken cancellationToken = default) => DefaultResolver.ResolvePtrAsync(name, cancellationToken); + /// + /// Asynchronously resolves the pointer (PTR) records for the specified IP address using the system-configured DNS servers. + /// + /// The IP address to perform a reverse lookup for. + /// A token to cancel the asynchronous operation. + /// A task that completes with a containing the PTR records. + /// is . public static Task> ResolvePtrAsync(IPAddress address, CancellationToken cancellationToken = default) - => DefaultResolver.ResolvePtrAsync(address, cancellationToken); + { + ArgumentNullException.ThrowIfNull(address); + return DefaultResolver.ResolvePtrAsync(DnsResolver.BuildArpaName(address), cancellationToken); + } + /// + /// Resolves the authoritative name server (NS) records for the specified name using the system-configured DNS servers. + /// + /// The domain name to resolve. + /// A containing the NS records. + /// is or empty. public static DnsResult ResolveNs(string name) => DefaultResolver.ResolveNs(name); + /// + /// Asynchronously resolves the authoritative name server (NS) records for the specified name using the system-configured DNS servers. + /// + /// The domain name to resolve. + /// A token to cancel the asynchronous operation. + /// A task that completes with a containing the NS records. + /// is or empty. public static Task> ResolveNsAsync(string name, CancellationToken cancellationToken = default) => DefaultResolver.ResolveNsAsync(name, cancellationToken); } diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsRecords.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsRecords.cs index dd31abed2b1e27..7a10fc3000c420 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsRecords.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsRecords.cs @@ -5,10 +5,12 @@ namespace System.Net { - /// An A or AAAA record resolved from DNS, with TTL. + /// Represents an A or AAAA record resolved from DNS, including its time-to-live. public readonly struct AddressRecord { + /// Gets the resolved IP address. public IPAddress Address { get; } + /// Gets the time-to-live (TTL) of the record. public TimeSpan Ttl { get; } internal AddressRecord(IPAddress address, TimeSpan ttl) @@ -18,19 +20,25 @@ internal AddressRecord(IPAddress address, TimeSpan ttl) } } - /// An SRV record (RFC 2782) with optional inlined address records from the additional section. + /// Represents an SRV record (RFC 2782), with optional inlined address records from the additional section. public readonly struct SrvRecord { private readonly IReadOnlyList? _addresses; + /// Gets the domain name of the target host. public string Target { get; } + /// Gets the port on the target host where the service is found. [CLSCompliant(false)] public ushort Port { get; } + /// Gets the priority of the target host. Lower values are preferred. [CLSCompliant(false)] public ushort Priority { get; } + /// Gets the relative weight for records with the same priority. [CLSCompliant(false)] public ushort Weight { get; } + /// Gets the time-to-live (TTL) of the record. public TimeSpan Ttl { get; } + /// Gets the address records for the target host that were included in the additional section of the response, if any. public IReadOnlyList Addresses => _addresses ?? Array.Empty(); internal SrvRecord(string target, ushort port, ushort priority, ushort weight, TimeSpan ttl, IReadOnlyList? addresses) @@ -44,12 +52,15 @@ internal SrvRecord(string target, ushort port, ushort priority, ushort weight, T } } - /// An MX record (RFC 1035 §3.3.9). + /// Represents an MX (mail exchange) record (RFC 1035 §3.3.9). public readonly struct MxRecord { + /// Gets the domain name of the mail exchange host. public string Exchange { get; } + /// Gets the preference of this mail exchange. Lower values are preferred. [CLSCompliant(false)] public ushort Preference { get; } + /// Gets the time-to-live (TTL) of the record. public TimeSpan Ttl { get; } internal MxRecord(string exchange, ushort preference, TimeSpan ttl) @@ -60,12 +71,14 @@ internal MxRecord(string exchange, ushort preference, TimeSpan ttl) } } - /// A TXT record (RFC 1035 §3.3.14). One record may carry multiple character-strings. + /// Represents a TXT record (RFC 1035 §3.3.14). A single record may carry multiple character-strings. public readonly struct TxtRecord { private readonly IReadOnlyList? _values; + /// Gets the character-strings contained in the record. public IReadOnlyList Values => _values ?? Array.Empty(); + /// Gets the time-to-live (TTL) of the record. public TimeSpan Ttl { get; } internal TxtRecord(IReadOnlyList values, TimeSpan ttl) @@ -75,10 +88,12 @@ internal TxtRecord(IReadOnlyList values, TimeSpan ttl) } } - /// A CNAME record (RFC 1035 §3.3.1). + /// Represents a CNAME (canonical name) record (RFC 1035 §3.3.1). public readonly struct CNameRecord { + /// Gets the canonical name for the queried name. public string CanonicalName { get; } + /// Gets the time-to-live (TTL) of the record. public TimeSpan Ttl { get; } internal CNameRecord(string canonicalName, TimeSpan ttl) @@ -88,10 +103,12 @@ internal CNameRecord(string canonicalName, TimeSpan ttl) } } - /// A PTR record (RFC 1035 §3.3.12). + /// Represents a PTR (pointer) record (RFC 1035 §3.3.12), typically used for reverse DNS lookups. public readonly struct PtrRecord { + /// Gets the domain name the queried name points to. public string Name { get; } + /// Gets the time-to-live (TTL) of the record. public TimeSpan Ttl { get; } internal PtrRecord(string name, TimeSpan ttl) @@ -101,10 +118,12 @@ internal PtrRecord(string name, TimeSpan ttl) } } - /// An NS record (RFC 1035 §3.3.11). + /// Represents an NS (name server) record (RFC 1035 §3.3.11). public readonly struct NsRecord { + /// Gets the domain name of the authoritative name server. public string Name { get; } + /// Gets the time-to-live (TTL) of the record. public TimeSpan Ttl { get; } internal NsRecord(string name, TimeSpan ttl) diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs index 483ae143f82fb4..0c079ad460e5a3 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs @@ -18,20 +18,50 @@ namespace System.Net /// public sealed partial class DnsResolver : IAsyncDisposable, IDisposable { - private readonly DnsResolverOptions _options; + private readonly IPEndPoint[] _servers; private bool _disposed; + /// + /// Initializes a new instance of the class that uses the + /// system-configured DNS servers. + /// public DnsResolver() : this(new DnsResolverOptions()) { } + /// + /// Initializes a new instance of the class with the specified options. + /// + /// The options controlling how DNS resolution is performed. + /// is . public DnsResolver(DnsResolverOptions options) { ArgumentNullException.ThrowIfNull(options); - _options = options; + // Capture a snapshot of the configured servers so later mutations of the + // options instance don't affect resolutions performed by this resolver. + _servers = [.. options.Servers]; } + /// + /// Resolves the IPv4 (A) and IPv6 (AAAA) addresses for the specified host name. + /// + /// The host name to resolve. + /// A containing the address records. + /// is or empty. + /// The resolver has been disposed. public DnsResult ResolveAddresses(string name) => ResolveAddresses(name, AddressFamily.Unspecified); + /// + /// Resolves the addresses of the specified family for the specified host name. + /// + /// The host name to resolve. + /// + /// The address family to query. Use for A records, + /// for AAAA records, or + /// for both. + /// + /// A containing the address records. + /// is or empty. + /// The resolver has been disposed. public DnsResult ResolveAddresses(string name, AddressFamily addressFamily) { ValidateName(name); @@ -41,6 +71,13 @@ public DnsResult ResolveAddresses(string name, AddressFamily addr return task.GetAwaiter().GetResult(); } + /// + /// Resolves the service (SRV) records for the specified name. + /// + /// The name to resolve, typically in the form _service._protocol.host. + /// A containing the SRV records. + /// is or empty. + /// The resolver has been disposed. public DnsResult ResolveSrv(string name) { ValidateName(name); @@ -50,6 +87,13 @@ public DnsResult ResolveSrv(string name) return task.GetAwaiter().GetResult(); } + /// + /// Resolves the mail exchange (MX) records for the specified name. + /// + /// The domain name to resolve. + /// A containing the MX records. + /// is or empty. + /// The resolver has been disposed. public DnsResult ResolveMx(string name) { ValidateName(name); @@ -59,6 +103,13 @@ public DnsResult ResolveMx(string name) return task.GetAwaiter().GetResult(); } + /// + /// Resolves the text (TXT) records for the specified name. + /// + /// The domain name to resolve. + /// A containing the TXT records. + /// is or empty. + /// The resolver has been disposed. public DnsResult ResolveTxt(string name) { ValidateName(name); @@ -68,6 +119,13 @@ public DnsResult ResolveTxt(string name) return task.GetAwaiter().GetResult(); } + /// + /// Resolves the canonical name (CNAME) record for the specified name. + /// + /// The domain name to resolve. + /// A containing the CNAME records. + /// is or empty. + /// The resolver has been disposed. public DnsResult ResolveCName(string name) { ValidateName(name); @@ -77,6 +135,13 @@ public DnsResult ResolveCName(string name) return task.GetAwaiter().GetResult(); } + /// + /// Resolves the pointer (PTR) records for the specified name, typically used for reverse DNS lookups. + /// + /// The name to resolve, typically a reverse-lookup name such as 4.3.2.1.in-addr.arpa. + /// A containing the PTR records. + /// is or empty. + /// The resolver has been disposed. public DnsResult ResolvePtr(string name) { ValidateName(name); @@ -86,6 +151,13 @@ public DnsResult ResolvePtr(string name) return task.GetAwaiter().GetResult(); } + /// + /// Resolves the authoritative name server (NS) records for the specified name. + /// + /// The domain name to resolve. + /// A containing the NS records. + /// is or empty. + /// The resolver has been disposed. public DnsResult ResolveNs(string name) { ValidateName(name); @@ -95,9 +167,30 @@ public DnsResult ResolveNs(string name) return task.GetAwaiter().GetResult(); } + /// + /// Asynchronously resolves the IPv4 (A) and IPv6 (AAAA) addresses for the specified host name. + /// + /// The host name to resolve. + /// A token to cancel the asynchronous operation. + /// A task that completes with a containing the address records. + /// is or empty. + /// The resolver has been disposed. public Task> ResolveAddressesAsync(string name, CancellationToken cancellationToken = default) => ResolveAddressesAsync(name, AddressFamily.Unspecified, cancellationToken); + /// + /// Asynchronously resolves the addresses of the specified family for the specified host name. + /// + /// The host name to resolve. + /// + /// The address family to query. Use for A records, + /// for AAAA records, or + /// for both. + /// + /// A token to cancel the asynchronous operation. + /// A task that completes with a containing the address records. + /// is or empty. + /// The resolver has been disposed. public Task> ResolveAddressesAsync(string name, AddressFamily addressFamily, CancellationToken cancellationToken = default) { ValidateName(name); @@ -105,6 +198,14 @@ public Task> ResolveAddressesAsync(string name, Address return ResolveAddressesCore(async: true, name, addressFamily, cancellationToken); } + /// + /// Asynchronously resolves the service (SRV) records for the specified name. + /// + /// The name to resolve, typically in the form _service._protocol.host. + /// A token to cancel the asynchronous operation. + /// A task that completes with a containing the SRV records. + /// is or empty. + /// The resolver has been disposed. public Task> ResolveSrvAsync(string name, CancellationToken cancellationToken = default) { ValidateName(name); @@ -112,6 +213,14 @@ public Task> ResolveSrvAsync(string name, CancellationToken return ResolveSrvCore(async: true, name, cancellationToken); } + /// + /// Asynchronously resolves the mail exchange (MX) records for the specified name. + /// + /// The domain name to resolve. + /// A token to cancel the asynchronous operation. + /// A task that completes with a containing the MX records. + /// is or empty. + /// The resolver has been disposed. public Task> ResolveMxAsync(string name, CancellationToken cancellationToken = default) { ValidateName(name); @@ -119,6 +228,14 @@ public Task> ResolveMxAsync(string name, CancellationToken c return ResolveMxCore(async: true, name, cancellationToken); } + /// + /// Asynchronously resolves the text (TXT) records for the specified name. + /// + /// The domain name to resolve. + /// A token to cancel the asynchronous operation. + /// A task that completes with a containing the TXT records. + /// is or empty. + /// The resolver has been disposed. public Task> ResolveTxtAsync(string name, CancellationToken cancellationToken = default) { ValidateName(name); @@ -126,6 +243,14 @@ public Task> ResolveTxtAsync(string name, CancellationToken return ResolveTxtCore(async: true, name, cancellationToken); } + /// + /// Asynchronously resolves the canonical name (CNAME) record for the specified name. + /// + /// The domain name to resolve. + /// A token to cancel the asynchronous operation. + /// A task that completes with a containing the CNAME records. + /// is or empty. + /// The resolver has been disposed. public Task> ResolveCNameAsync(string name, CancellationToken cancellationToken = default) { ValidateName(name); @@ -133,6 +258,14 @@ public Task> ResolveCNameAsync(string name, CancellationT return ResolveCNameCore(async: true, name, cancellationToken); } + /// + /// Asynchronously resolves the pointer (PTR) records for the specified name, typically used for reverse DNS lookups. + /// + /// The name to resolve, typically a reverse-lookup name such as 4.3.2.1.in-addr.arpa. + /// A token to cancel the asynchronous operation. + /// A task that completes with a containing the PTR records. + /// is or empty. + /// The resolver has been disposed. public Task> ResolvePtrAsync(string name, CancellationToken cancellationToken = default) { ValidateName(name); @@ -140,13 +273,14 @@ public Task> ResolvePtrAsync(string name, CancellationToken return ResolvePtrCore(async: true, name, cancellationToken); } - public Task> ResolvePtrAsync(IPAddress address, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(address); - ObjectDisposedException.ThrowIf(_disposed, this); - return ResolvePtrCore(async: true, BuildArpaName(address), cancellationToken); - } - + /// + /// Asynchronously resolves the authoritative name server (NS) records for the specified name. + /// + /// The domain name to resolve. + /// A token to cancel the asynchronous operation. + /// A task that completes with a containing the NS records. + /// is or empty. + /// The resolver has been disposed. public Task> ResolveNsAsync(string name, CancellationToken cancellationToken = default) { ValidateName(name); @@ -154,8 +288,10 @@ public Task> ResolveNsAsync(string name, CancellationToken c return ResolveNsCore(async: true, name, cancellationToken); } + /// public void Dispose() => _disposed = true; + /// public ValueTask DisposeAsync() { _disposed = true; @@ -176,53 +312,67 @@ public ValueTask DisposeAsync() private Task> ResolveAddressesCore(bool async, string name, AddressFamily addressFamily, CancellationToken cancellationToken) => NameResolutionTelemetry.AnyDiagnosticsEnabled() - ? ResolveWithTelemetry(name, () => DnsResolverPal.ResolveAddresses(_options.Servers, async, name, addressFamily, cancellationToken), static r => MapAnswers(r, static a => a.Address.ToString())) - : DnsResolverPal.ResolveAddresses(_options.Servers, async, name, addressFamily, cancellationToken); + ? ResolveWithTelemetry(name, (servers: _servers, async, name, addressFamily, cancellationToken), + static s => DnsResolverPal.ResolveAddresses(s.servers, s.async, s.name, s.addressFamily, s.cancellationToken), + static r => MapAnswers(r, static a => a.Address.ToString())) + : DnsResolverPal.ResolveAddresses(_servers, async, name, addressFamily, cancellationToken); private Task> ResolveSrvCore(bool async, string name, CancellationToken cancellationToken) => NameResolutionTelemetry.AnyDiagnosticsEnabled() - ? ResolveWithTelemetry(name, () => DnsResolverPal.ResolveSrv(_options.Servers, async, name, cancellationToken), static r => MapAnswers(r, static a => a.Target)) - : DnsResolverPal.ResolveSrv(_options.Servers, async, name, cancellationToken); + ? ResolveWithTelemetry(name, (servers: _servers, async, name, cancellationToken), + static s => DnsResolverPal.ResolveSrv(s.servers, s.async, s.name, s.cancellationToken), + static r => MapAnswers(r, static a => a.Target)) + : DnsResolverPal.ResolveSrv(_servers, async, name, cancellationToken); private Task> ResolveMxCore(bool async, string name, CancellationToken cancellationToken) => NameResolutionTelemetry.AnyDiagnosticsEnabled() - ? ResolveWithTelemetry(name, () => DnsResolverPal.ResolveMx(_options.Servers, async, name, cancellationToken), static r => MapAnswers(r, static a => a.Exchange)) - : DnsResolverPal.ResolveMx(_options.Servers, async, name, cancellationToken); + ? ResolveWithTelemetry(name, (servers: _servers, async, name, cancellationToken), + static s => DnsResolverPal.ResolveMx(s.servers, s.async, s.name, s.cancellationToken), + static r => MapAnswers(r, static a => a.Exchange)) + : DnsResolverPal.ResolveMx(_servers, async, name, cancellationToken); private Task> ResolveTxtCore(bool async, string name, CancellationToken cancellationToken) => NameResolutionTelemetry.AnyDiagnosticsEnabled() - ? ResolveWithTelemetry(name, () => DnsResolverPal.ResolveTxt(_options.Servers, async, name, cancellationToken), static r => - { - List values = new(); - foreach (TxtRecord record in r.Records) + ? ResolveWithTelemetry(name, (servers: _servers, async, name, cancellationToken), + static s => DnsResolverPal.ResolveTxt(s.servers, s.async, s.name, s.cancellationToken), + static r => { - values.AddRange(record.Values); - } - return values.ToArray(); - }) - : DnsResolverPal.ResolveTxt(_options.Servers, async, name, cancellationToken); + List values = new(); + foreach (TxtRecord record in r.Records) + { + values.AddRange(record.Values); + } + return values.ToArray(); + }) + : DnsResolverPal.ResolveTxt(_servers, async, name, cancellationToken); private Task> ResolveCNameCore(bool async, string name, CancellationToken cancellationToken) => NameResolutionTelemetry.AnyDiagnosticsEnabled() - ? ResolveWithTelemetry(name, () => DnsResolverPal.ResolveCName(_options.Servers, async, name, cancellationToken), static r => MapAnswers(r, static a => a.CanonicalName)) - : DnsResolverPal.ResolveCName(_options.Servers, async, name, cancellationToken); + ? ResolveWithTelemetry(name, (servers: _servers, async, name, cancellationToken), + static s => DnsResolverPal.ResolveCName(s.servers, s.async, s.name, s.cancellationToken), + static r => MapAnswers(r, static a => a.CanonicalName)) + : DnsResolverPal.ResolveCName(_servers, async, name, cancellationToken); private Task> ResolvePtrCore(bool async, string name, CancellationToken cancellationToken) => NameResolutionTelemetry.AnyDiagnosticsEnabled() - ? ResolveWithTelemetry(name, () => DnsResolverPal.ResolvePtr(_options.Servers, async, name, cancellationToken), static r => MapAnswers(r, static a => a.Name)) - : DnsResolverPal.ResolvePtr(_options.Servers, async, name, cancellationToken); + ? ResolveWithTelemetry(name, (servers: _servers, async, name, cancellationToken), + static s => DnsResolverPal.ResolvePtr(s.servers, s.async, s.name, s.cancellationToken), + static r => MapAnswers(r, static a => a.Name)) + : DnsResolverPal.ResolvePtr(_servers, async, name, cancellationToken); private Task> ResolveNsCore(bool async, string name, CancellationToken cancellationToken) => NameResolutionTelemetry.AnyDiagnosticsEnabled() - ? ResolveWithTelemetry(name, () => DnsResolverPal.ResolveNs(_options.Servers, async, name, cancellationToken), static r => MapAnswers(r, static a => a.Name)) - : DnsResolverPal.ResolveNs(_options.Servers, async, name, cancellationToken); + ? ResolveWithTelemetry(name, (servers: _servers, async, name, cancellationToken), + static s => DnsResolverPal.ResolveNs(s.servers, s.async, s.name, s.cancellationToken), + static r => MapAnswers(r, static a => a.Name)) + : DnsResolverPal.ResolveNs(_servers, async, name, cancellationToken); - private static async Task> ResolveWithTelemetry(string name, Func>> resolve, Func, string[]> getAnswers) + private static async Task> ResolveWithTelemetry(string name, TState state, Func>> resolve, Func, string[]> getAnswers) { NameResolutionActivity activity = NameResolutionTelemetry.Log.BeforeResolution(name); try { - DnsResult result = await resolve().ConfigureAwait(false); + DnsResult result = await resolve(state).ConfigureAwait(false); NameResolutionTelemetry.Log.AfterResolution(name, in activity, getAnswers(result)); return result; } @@ -258,13 +408,13 @@ internal static unsafe string BuildArpaName(IPAddress address) { Span 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"); + return $"{bytes[3]}.{bytes[2]}.{bytes[1]}.{bytes[0]}.in-addr.arpa"; } else if (address.AddressFamily == AddressFamily.InterNetworkV6) { Span bytes = stackalloc byte[16]; address.TryWriteBytes(bytes, out _); - Span chars = stackalloc char[32 * 2 + 9]; + Span chars = stackalloc char[16 * 4]; int pos = 0; for (int i = 15; i >= 0; i--) { @@ -274,9 +424,7 @@ internal static unsafe string BuildArpaName(IPAddress address) 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)); + return string.Concat(chars, "ip6.arpa"); static char ToHex(int n) => (char)(n < 10 ? '0' + n : 'a' + (n - 10)); } diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverOptions.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverOptions.cs index d9bb6c9dffbf6b..2f20f8064a2e41 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverOptions.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverOptions.cs @@ -10,9 +10,20 @@ namespace System.Net /// public sealed class DnsResolverOptions { + private IList _servers = new List(); + /// - /// DNS servers to query. When empty, the system-configured DNS servers are used. + /// Gets or sets the DNS servers to query. When empty, the system-configured DNS servers are used. /// - public IList Servers { get; set; } = new List(); + /// The value being set is . + public IList Servers + { + get => _servers; + set + { + ArgumentNullException.ThrowIfNull(value); + _servers = value; + } + } } } diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs index 16264e8ddd314c..42e51902629f34 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs @@ -1,7 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers.Binary; using System.Collections.Generic; +using System.Diagnostics; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Threading; @@ -48,15 +50,8 @@ public static async Task> ResolveAddresses(IList> ResolveSrv(IList servers, bool async, string name, CancellationToken cancellationToken) { - DnsQueryRawResult raw = await DnsQueryEx(servers, async, name, Interop.Dnsapi.DNS_TYPE_SRV, cancellationToken).ConfigureAwait(false); - try - { - return ParseSrv(raw); - } - finally - { - raw.Dispose(); - } + using DnsQueryRawResult raw = await DnsQueryEx(servers, async, name, Interop.Dnsapi.DNS_TYPE_SRV, cancellationToken).ConfigureAwait(false); + return ParseSrv(raw); } public static Task> ResolveMx(IList servers, bool async, string name, CancellationToken cancellationToken) @@ -73,15 +68,8 @@ public static Task> ResolveNs(IList servers, boo public static async Task> ResolveTxt(IList servers, bool async, string name, CancellationToken cancellationToken) { - DnsQueryRawResult raw = await DnsQueryEx(servers, async, name, Interop.Dnsapi.DNS_TYPE_TEXT, cancellationToken).ConfigureAwait(false); - try - { - return ParseTxt(raw); - } - finally - { - raw.Dispose(); - } + using DnsQueryRawResult raw = await DnsQueryEx(servers, async, name, Interop.Dnsapi.DNS_TYPE_TEXT, cancellationToken).ConfigureAwait(false); + return ParseTxt(raw); } // ---- Per-record-type selectors (shared by all record types) ---- @@ -122,29 +110,15 @@ private static ushort AddressFamilyToQueryType(AddressFamily addressFamily) => private static async Task> QueryAddresses(IList servers, bool async, string name, ushort qtype, CancellationToken cancellationToken) { - DnsQueryRawResult raw = await DnsQueryEx(servers, async, name, qtype, cancellationToken).ConfigureAwait(false); - try - { - return ParseAddresses(raw, qtype); - } - finally - { - raw.Dispose(); - } + using DnsQueryRawResult raw = await DnsQueryEx(servers, async, name, qtype, cancellationToken).ConfigureAwait(false); + return ParseAddresses(raw, qtype); } private static async Task> QuerySimple(IList servers, bool async, string name, ushort qtype, CancellationToken cancellationToken, Func selector) { - DnsQueryRawResult raw = await DnsQueryEx(servers, async, name, qtype, cancellationToken).ConfigureAwait(false); - try - { - return ParseSimple(raw, qtype, selector); - } - finally - { - raw.Dispose(); - } + using DnsQueryRawResult raw = await DnsQueryEx(servers, async, name, qtype, cancellationToken).ConfigureAwait(false); + return ParseSimple(raw, qtype, selector); } // ---- Record-list parsers ---- @@ -172,7 +146,7 @@ private static DnsResult ParseAddresses(DnsQueryRawResult raw, us cur = hdr.pNext; } - return new DnsResult(DnsResponseCode.NoError, records, TimeSpan.Zero); + return new DnsResult(DnsResponseCode.NoError, records, raw.NegativeCacheTtl); } private static DnsResult ParseSrv(DnsQueryRawResult raw) @@ -196,17 +170,14 @@ private static DnsResult ParseSrv(DnsQueryRawResult raw) IntPtr dataPtr = cur + Marshal.SizeOf(); Interop.Dnsapi.DNS_SRV_DATA data = Marshal.PtrToStructure(dataPtr); string target = PtrToString(data.pNameTarget) ?? string.Empty; - IReadOnlyList? attached = null; - if (glue != null && glue.TryGetValue(target, out List? list)) - { - attached = list; - } + List? attached = null; + glue?.TryGetValue(target, out attached); records.Add(new SrvRecord(target, data.wPort, data.wPriority, data.wWeight, TimeSpan.FromSeconds(hdr.dwTtl), attached)); } cur = hdr.pNext; } - return new DnsResult(DnsResponseCode.NoError, records, TimeSpan.Zero); + return new DnsResult(DnsResponseCode.NoError, records, raw.NegativeCacheTtl); } private static DnsResult ParseTxt(DnsQueryRawResult raw) @@ -245,7 +216,7 @@ private static DnsResult ParseTxt(DnsQueryRawResult raw) cur = hdr.pNext; } - return new DnsResult(DnsResponseCode.NoError, records, TimeSpan.Zero); + return new DnsResult(DnsResponseCode.NoError, records, raw.NegativeCacheTtl); } private static DnsResult ParseSimple(DnsQueryRawResult raw, ushort qtype, @@ -269,18 +240,16 @@ private static DnsResult ParseSimple(DnsQueryRawResult raw, us cur = hdr.pNext; } - return new DnsResult(DnsResponseCode.NoError, records, TimeSpan.Zero); + return new DnsResult(DnsResponseCode.NoError, records, raw.NegativeCacheTtl); } private static DnsResult MergeAddressResults(DnsResult a, DnsResult b) { if (a.Records.Count > 0 || b.Records.Count > 0) { - AddressRecord[] merged = new AddressRecord[a.Records.Count + b.Records.Count]; - int idx = 0; - for (int i = 0; i < a.Records.Count; i++) merged[idx++] = a.Records[i]; - for (int i = 0; i < b.Records.Count; i++) merged[idx++] = b.Records[i]; - return new DnsResult(DnsResponseCode.NoError, merged, TimeSpan.Zero); + AddressRecord[] merged = [.. a.Records, .. b.Records]; + TimeSpan mergedTtl = a.NegativeCacheTtl > b.NegativeCacheTtl ? a.NegativeCacheTtl : b.NegativeCacheTtl; + return new DnsResult(DnsResponseCode.NoError, merged, mergedTtl); } DnsResponseCode chosenRc = a.ResponseCode == DnsResponseCode.NxDomain || b.ResponseCode == DnsResponseCode.NxDomain @@ -290,21 +259,14 @@ private static DnsResult MergeAddressResults(DnsResult(chosenRc, null, negTtl); } - private static bool TryParseAddress(ushort recordType, IntPtr dataPtr, out IPAddress? address) + private static unsafe bool TryParseAddress(ushort recordType, IntPtr dataPtr, out IPAddress? address) { - if (recordType == Interop.Dnsapi.DNS_TYPE_A) - { - uint ip = (uint)Marshal.ReadInt32(dataPtr); - address = new IPAddress(ip); - return true; - } - if (recordType == Interop.Dnsapi.DNS_TYPE_AAAA) + if (recordType is Interop.Dnsapi.DNS_TYPE_A or Interop.Dnsapi.DNS_TYPE_AAAA) { - byte[] bytes = new byte[16]; - Marshal.Copy(dataPtr, bytes, 0, 16); - address = new IPAddress(bytes); + address = new IPAddress(new ReadOnlySpan((byte*)dataPtr, recordType == Interop.Dnsapi.DNS_TYPE_A ? 4 : 16)); return true; } + address = null; return false; } @@ -323,11 +285,7 @@ private static void ParseAdditionalAddresses(IntPtr head, ref Dictionary>(StringComparer.OrdinalIgnoreCase); - if (!glue.TryGetValue(name, out List? list)) - { - list = new List(); - glue[name] = list; - } + List list = CollectionsMarshal.GetValueRefOrAddDefault(glue, name, out _) ??= new List(); list.Add(new AddressRecord(address!, TimeSpan.FromSeconds(hdr.dwTtl))); } } @@ -379,14 +337,14 @@ private sealed unsafe class DnsQueryAsyncState private readonly CancellationToken _cancellationToken; private readonly IList _servers; - private GCHandle _selfHandle; + private GCHandle _selfHandle; private IntPtr _namePtr; private IntPtr _requestPtr; private IntPtr _resultPtr; private IntPtr _cancelPtr; private IntPtr _serverListPtr; // DNS_ADDR_ARRAY buffer private CancellationTokenRegistration _ctReg; - private int _completed; // 0 = pending, 1 = completed (callback or sync) + private bool _completed; public DnsQueryAsyncState(IList servers, string name, ushort queryType, CancellationToken cancellationToken) { @@ -411,7 +369,7 @@ public Task StartAsync() _cancelPtr = Marshal.AllocHGlobal(sizeof(Interop.Dnsapi.DNS_QUERY_CANCEL)); NativeMemory.Clear((void*)_cancelPtr, (nuint)sizeof(Interop.Dnsapi.DNS_QUERY_CANCEL)); - _selfHandle = GCHandle.Alloc(this); + _selfHandle = new GCHandle(this); _requestPtr = Marshal.AllocHGlobal(sizeof(Interop.Dnsapi.DNS_QUERY_REQUEST)); NativeMemory.Clear((void*)_requestPtr, (nuint)sizeof(Interop.Dnsapi.DNS_QUERY_REQUEST)); @@ -422,7 +380,7 @@ public Task StartAsync() req->QueryOptions = Interop.Dnsapi.DNS_QUERY_STANDARD; req->InterfaceIndex = 0; req->pQueryCompletionCallback = s_completionCallbackPtr; - req->pQueryContext = GCHandle.ToIntPtr(_selfHandle); + req->pQueryContext = GCHandle.ToIntPtr(_selfHandle); if (_servers is { Count: > 0 }) { @@ -477,7 +435,7 @@ private void CancelAndAbort() /// internal void CompleteFromResult(int status) { - if (Interlocked.Exchange(ref _completed, 1) != 0) + if (Interlocked.Exchange(ref _completed, true)) { return; } @@ -501,13 +459,10 @@ internal void CompleteFromResult(int status) DnsResponseCode rc = MapWindowsErrorToResponseCode(status); - // 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); - } + // Extract the negative-cache TTL from an authority-section SOA when present. + // This covers both NXDOMAIN and NODATA (the latter maps to NoError but can + // still carry an authority SOA); the helper returns zero when no SOA is found. + TimeSpan negativeTtl = ExtractNegativeCacheTtl(records); _tcs.TrySetResult(new DnsQueryRawResult(rc, records, negativeTtl)); } @@ -550,7 +505,7 @@ private void FreeAll() } if (_selfHandle.IsAllocated) { - _selfHandle.Free(); + _selfHandle.Dispose(); } } } @@ -562,12 +517,7 @@ private static void QueryCompletionCallback(IntPtr pQueryContext, IntPtr pQueryR { try { - GCHandle handle = GCHandle.FromIntPtr(pQueryContext); - DnsQueryAsyncState? state = handle.Target as DnsQueryAsyncState; - if (state == null) - { - return; - } + DnsQueryAsyncState state = GCHandle.FromIntPtr(pQueryContext).Target; // pQueryResults points to the same DNS_QUERY_RESULT we passed in. unsafe @@ -576,9 +526,10 @@ private static void QueryCompletionCallback(IntPtr pQueryContext, IntPtr pQueryR state.CompleteFromResult(res->QueryStatus); } } - catch + catch (Exception ex) { - // Swallow — never allow exceptions to propagate into native code. + // Never allow exceptions to propagate into native code. + Debug.Fail($"Unexpected exception in DnsQueryEx completion callback: {ex}"); } } @@ -589,14 +540,11 @@ private static void QueryCompletionCallback(IntPtr pQueryContext, IntPtr pQueryR // requests a non-default port, since it cannot be honored on Windows. private static void ValidateServerPorts(IList servers) { - if (servers is { Count: > 0 }) + foreach (IPEndPoint ep in servers) { - foreach (IPEndPoint ep in servers) + if (ep.Port != 0 && ep.Port != 53) { - if (ep.Port != 0 && ep.Port != 53) - { - throw new PlatformNotSupportedException(SR.net_dns_custom_port_not_supported); - } + throw new PlatformNotSupportedException(SR.net_dns_custom_port_not_supported); } } } @@ -648,13 +596,10 @@ private static unsafe DnsQueryRawResult DnsQueryExSync(IList servers DnsResponseCode rc = MapWindowsErrorToResponseCode(status); - // 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); - } + // Extract the negative-cache TTL from an authority-section SOA when present. + // This covers both NXDOMAIN and NODATA (the latter maps to NoError but can + // still carry an authority SOA); the helper returns zero when no SOA is found. + TimeSpan negativeTtl = ExtractNegativeCacheTtl(records); return new DnsQueryRawResult(rc, records, negativeTtl); } @@ -674,9 +619,22 @@ private static unsafe DnsQueryRawResult DnsQueryExSync(IList servers private static unsafe void BuildAddrArray(IList servers, out IntPtr arrayPtr) { int count = servers.Count; + + // DnsQueryEx encodes a single address family for the whole array, so all + // endpoints must share one. Reject mixed IPv4/IPv6 lists up front instead + // of producing an inconsistent DNS_ADDR_ARRAY. + AddressFamily family = servers[0].AddressFamily; + for (int i = 1; i < count; i++) + { + if (servers[i].AddressFamily != family) + { + throw new ArgumentException(SR.net_dns_mixed_address_families, nameof(servers)); + } + } + int headerSize = sizeof(Interop.Dnsapi.DNS_ADDR_ARRAY); int addrSize = sizeof(Interop.Dnsapi.DNS_ADDR); - int totalSize = headerSize + addrSize * count; + int totalSize = checked(headerSize + addrSize * count); arrayPtr = Marshal.AllocHGlobal(totalSize); NativeMemory.Clear((void*)arrayPtr, (nuint)totalSize); @@ -684,56 +642,42 @@ private static unsafe void BuildAddrArray(IList servers, out IntPtr Interop.Dnsapi.DNS_ADDR_ARRAY* arr = (Interop.Dnsapi.DNS_ADDR_ARRAY*)arrayPtr; arr->MaxCount = (uint)count; arr->AddrCount = (uint)count; - arr->Family = (ushort)(servers[0].AddressFamily == AddressFamily.InterNetwork ? Interop.Dnsapi.AF_INET : Interop.Dnsapi.AF_INET6); + arr->Family = (ushort)(family == AddressFamily.InterNetwork ? Interop.Dnsapi.AF_INET : Interop.Dnsapi.AF_INET6); byte* addrBase = (byte*)arrayPtr + headerSize; for (int i = 0; i < count; i++) { - IPEndPoint ep = servers[i]; byte* sa = addrBase + (i * addrSize); - WriteSockAddr(sa, ep); + WriteSockAddr(sa, servers[i].Address); } } // 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) + private static unsafe void WriteSockAddr(byte* dest, IPAddress address) { // DnsQueryEx always queries DNS servers on port 53 and requires the sockaddr // port field to be left as 0. Supplying a non-zero port (even 53) is rejected // with ERROR_INVALID_PARAMETER. Non-default ports are validated and rejected // earlier, so the port is always written as 0 here. - if (ep.AddressFamily == AddressFamily.InterNetwork) + if (address.AddressFamily == AddressFamily.InterNetwork) { // sockaddr_in: ushort family, ushort port (net order), uint addr, 8 bytes zero *(ushort*)(dest + 0) = Interop.Dnsapi.AF_INET; // dest[2..3] (port) left zero - Span 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]; + address.TryWriteBytes(new Span(dest + 4, 4), out _); // dest[8..15] left zero } - else if (ep.AddressFamily == AddressFamily.InterNetworkV6) + else if (address.AddressFamily == AddressFamily.InterNetworkV6) { // sockaddr_in6: ushort family, ushort port, uint flowinfo, 16-byte addr, uint scope_id *(ushort*)(dest + 0) = Interop.Dnsapi.AF_INET6; // dest[2..3] (port) left zero // flowinfo (dest[4..7]) left zero - Span addrBytes = stackalloc byte[16]; - ep.Address.TryWriteBytes(addrBytes, out _); - for (int i = 0; i < 16; i++) - { - dest[8 + i] = addrBytes[i]; - } - // scope_id (dest[24..27]) - uint scopeId = (uint)ep.Address.ScopeId; - dest[24] = (byte)(scopeId & 0xff); - dest[25] = (byte)((scopeId >> 8) & 0xff); - dest[26] = (byte)((scopeId >> 16) & 0xff); - dest[27] = (byte)((scopeId >> 24) & 0xff); + address.TryWriteBytes(new Span(dest + 8, 16), out _); + // scope_id (dest[24..27]) is in host byte order; this code is Windows-only + // (always little-endian), so write it as little-endian. + BinaryPrimitives.WriteUInt32LittleEndian(new Span(dest + 24, 4), (uint)address.ScopeId); } else { diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResult.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResult.cs index 702445b4166d65..42f1b940c0fa0b 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResult.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResult.cs @@ -6,28 +6,30 @@ namespace System.Net { /// - /// Carries the result of a DNS resolution operation, including the response + /// Represents the result of a DNS resolution operation, including the response /// code, the parsed records, and (for negative responses) the negative-cache TTL. /// + /// The type of the resolved records. public readonly struct DnsResult { private readonly IReadOnlyList? _records; - /// The DNS response code returned by the server. + /// Gets the DNS response code returned by the server. [CLSCompliant(false)] public DnsResponseCode ResponseCode { get; } /// - /// The records returned by the server. Empty on error or NODATA responses. + /// Gets the records returned by the server. The list is empty on error or NODATA responses. /// public IReadOnlyList Records => _records ?? Array.Empty(); /// - /// For negative responses (NXDOMAIN/NODATA), the TTL for which the negative - /// answer may be cached (derived from the SOA minimum TTL in the authority - /// section, per RFC 2308 §5). if not applicable - /// or unavailable. + /// Gets the duration for which a negative response (NXDOMAIN or NODATA) may be cached. /// + /// + /// The value is derived from the SOA minimum TTL in the authority section, per RFC 2308 §5. + /// It is when not applicable or unavailable. + /// public TimeSpan NegativeCacheTtl { get; } internal DnsResult(DnsResponseCode responseCode, IReadOnlyList? records, TimeSpan negativeCacheTtl) diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs index eed201f7612d1c..f0effd738aae1a 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs @@ -195,6 +195,9 @@ public async Task ResolveAddresses_Nxdomain_ReturnsNxDomain(bool async) Assert.Equal(DnsResponseCode.NxDomain, result.ResponseCode); Assert.Empty(result.Records); + // The negative-cache TTL should be derived from the authority SOA record (120s). + Assert.True(result.NegativeCacheTtl > TimeSpan.Zero && result.NegativeCacheTtl <= TimeSpan.FromSeconds(120), + $"Unexpected NegativeCacheTtl: {result.NegativeCacheTtl}"); } [Theory] @@ -214,6 +217,9 @@ public async Task ResolveAddresses_NoData_ReturnsNoErrorWithEmptyRecords(bool as Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); Assert.Empty(result.Records); + // The negative-cache TTL should be derived from the authority SOA record (30s). + Assert.True(result.NegativeCacheTtl > TimeSpan.Zero && result.NegativeCacheTtl <= TimeSpan.FromSeconds(30), + $"Unexpected NegativeCacheTtl: {result.NegativeCacheTtl}"); } [Theory] diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs index be1f04eacb88a0..7e98dfe70db965 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs @@ -46,7 +46,7 @@ public async Task DnsResolver_NullName_Throws() await Assert.ThrowsAsync(() => r.ResolveTxtAsync(null!)); await Assert.ThrowsAsync(() => r.ResolveCNameAsync(null!)); await Assert.ThrowsAsync(() => r.ResolvePtrAsync((string)null!)); - await Assert.ThrowsAsync(() => r.ResolvePtrAsync((IPAddress)null!)); + await Assert.ThrowsAsync(() => Dns.ResolvePtrAsync((IPAddress)null!)); await Assert.ThrowsAsync(() => r.ResolveNsAsync(null!)); } @@ -201,8 +201,7 @@ public async Task ResolveNs_KnownName_ReturnsRecords() [OuterLoop] public async Task ResolvePtr_ByIPAddress_ReturnsRecord() { - using DnsResolver r = new DnsResolver(); - DnsResult result = await r.ResolvePtrAsync(IPAddress.Parse("8.8.8.8")); + DnsResult result = await Dns.ResolvePtrAsync(IPAddress.Parse("8.8.8.8")); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); Assert.NotEmpty(result.Records); Assert.False(string.IsNullOrEmpty(result.Records[0].Name)); @@ -221,10 +220,16 @@ public async Task Static_Dns_ResolveAddressesAsync_Works() [OuterLoop] public async Task DnsResolver_CustomServer_Port53_Works() { - IPAddress dnsAddress = System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces() + IPAddress? dnsAddress = System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces() .SelectMany(ni => ni.GetIPProperties().DnsAddresses) .FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetwork); + if (dnsAddress is null) + { + // No IPv4 DNS server is configured on this machine; nothing to validate. + return; + } + DnsResolverOptions opts = new DnsResolverOptions { Servers = { new IPEndPoint(dnsAddress, 53) } @@ -254,9 +259,8 @@ public async Task DnsResolver_CustomServer_NonStandardPort_ThrowsPlatformNotSupp [OuterLoop] public async Task ResolvePtr_IPv6Address_DoesNotThrow() { - using DnsResolver r = new DnsResolver(); // Google public DNS IPv6 — call shouldn't throw, even if no PTR record exists. - DnsResult result = await r.ResolvePtrAsync(IPAddress.Parse("2001:4860:4860::8888")); + DnsResult result = await Dns.ResolvePtrAsync(IPAddress.Parse("2001:4860:4860::8888")); Assert.True(result.ResponseCode == DnsResponseCode.NoError || result.ResponseCode == DnsResponseCode.NxDomain); } } From 42bb74eff71695850845f7dafa7f32797ec5c878 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Tue, 16 Jun 2026 15:47:56 +0200 Subject: [PATCH 12/19] Keep instance DnsResolver.ResolvePtr(IPAddress) overloads The approved API shape was corrected to include the IPAddress reverse-lookup overloads on the instance resolver, so restore ResolvePtr(IPAddress) and ResolvePtrAsync(IPAddress) on DnsResolver. The static Dns entry points now delegate to them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ref/System.Net.NameResolution.cs | 2 ++ .../src/System/Net/Dns.Resolve.cs | 10 ++---- .../src/System/Net/DnsResolver.cs | 31 +++++++++++++++++++ .../tests/FunctionalTests/DnsResolverTest.cs | 8 +++-- 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/libraries/System.Net.NameResolution/ref/System.Net.NameResolution.cs b/src/libraries/System.Net.NameResolution/ref/System.Net.NameResolution.cs index ae5580a4fc268b..4d3b37ae9d506c 100644 --- a/src/libraries/System.Net.NameResolution/ref/System.Net.NameResolution.cs +++ b/src/libraries/System.Net.NameResolution/ref/System.Net.NameResolution.cs @@ -79,6 +79,7 @@ public DnsResolver(System.Net.DnsResolverOptions options) { } public System.Net.DnsResult ResolveTxt(string name) { throw null; } public System.Net.DnsResult ResolveCName(string name) { throw null; } public System.Net.DnsResult ResolvePtr(string name) { throw null; } + public System.Net.DnsResult ResolvePtr(System.Net.IPAddress address) { throw null; } public System.Net.DnsResult ResolveNs(string name) { throw null; } public System.Threading.Tasks.Task> ResolveAddressesAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public System.Threading.Tasks.Task> ResolveAddressesAsync(string name, System.Net.Sockets.AddressFamily addressFamily, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } @@ -87,6 +88,7 @@ public DnsResolver(System.Net.DnsResolverOptions options) { } public System.Threading.Tasks.Task> ResolveTxtAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public System.Threading.Tasks.Task> ResolveCNameAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public System.Threading.Tasks.Task> ResolvePtrAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public System.Threading.Tasks.Task> ResolvePtrAsync(System.Net.IPAddress address, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public System.Threading.Tasks.Task> ResolveNsAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public void Dispose() { } public System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/Dns.Resolve.cs b/src/libraries/System.Net.NameResolution/src/System/Net/Dns.Resolve.cs index 709265796795b6..6a5943d681e859 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/Dns.Resolve.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/Dns.Resolve.cs @@ -156,10 +156,7 @@ public static DnsResult ResolvePtr(string name) /// A containing the PTR records. /// is . public static DnsResult ResolvePtr(IPAddress address) - { - ArgumentNullException.ThrowIfNull(address); - return DefaultResolver.ResolvePtr(DnsResolver.BuildArpaName(address)); - } + => DefaultResolver.ResolvePtr(address); /// /// Asynchronously resolves the pointer (PTR) records for the specified name using the system-configured DNS servers. @@ -179,10 +176,7 @@ public static Task> ResolvePtrAsync(string name, Cancellati /// A task that completes with a containing the PTR records. /// is . public static Task> ResolvePtrAsync(IPAddress address, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(address); - return DefaultResolver.ResolvePtrAsync(DnsResolver.BuildArpaName(address), cancellationToken); - } + => DefaultResolver.ResolvePtrAsync(address, cancellationToken); /// /// Resolves the authoritative name server (NS) records for the specified name using the system-configured DNS servers. diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs index 0c079ad460e5a3..cc1b63e051b3ef 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs @@ -151,6 +151,22 @@ public DnsResult ResolvePtr(string name) return task.GetAwaiter().GetResult(); } + /// + /// Resolves the pointer (PTR) records for the specified IP address, performing a reverse DNS lookup. + /// + /// The IP address to perform a reverse lookup for. + /// A containing the PTR records. + /// is . + /// The resolver has been disposed. + public DnsResult ResolvePtr(IPAddress address) + { + ArgumentNullException.ThrowIfNull(address); + ObjectDisposedException.ThrowIf(_disposed, this); + Task> task = ResolvePtrCore(async: false, BuildArpaName(address), default); + Debug.Assert(task.IsCompleted); + return task.GetAwaiter().GetResult(); + } + /// /// Resolves the authoritative name server (NS) records for the specified name. /// @@ -273,6 +289,21 @@ public Task> ResolvePtrAsync(string name, CancellationToken return ResolvePtrCore(async: true, name, cancellationToken); } + /// + /// Asynchronously resolves the pointer (PTR) records for the specified IP address, performing a reverse DNS lookup. + /// + /// The IP address to perform a reverse lookup for. + /// A token to cancel the asynchronous operation. + /// A task that completes with a containing the PTR records. + /// is . + /// The resolver has been disposed. + public Task> ResolvePtrAsync(IPAddress address, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(address); + ObjectDisposedException.ThrowIf(_disposed, this); + return ResolvePtrCore(async: true, BuildArpaName(address), cancellationToken); + } + /// /// Asynchronously resolves the authoritative name server (NS) records for the specified name. /// diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs index 7e98dfe70db965..c7ac8895b3afca 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs @@ -46,7 +46,7 @@ public async Task DnsResolver_NullName_Throws() await Assert.ThrowsAsync(() => r.ResolveTxtAsync(null!)); await Assert.ThrowsAsync(() => r.ResolveCNameAsync(null!)); await Assert.ThrowsAsync(() => r.ResolvePtrAsync((string)null!)); - await Assert.ThrowsAsync(() => Dns.ResolvePtrAsync((IPAddress)null!)); + await Assert.ThrowsAsync(() => r.ResolvePtrAsync((IPAddress)null!)); await Assert.ThrowsAsync(() => r.ResolveNsAsync(null!)); } @@ -201,7 +201,8 @@ public async Task ResolveNs_KnownName_ReturnsRecords() [OuterLoop] public async Task ResolvePtr_ByIPAddress_ReturnsRecord() { - DnsResult result = await Dns.ResolvePtrAsync(IPAddress.Parse("8.8.8.8")); + using DnsResolver r = new DnsResolver(); + DnsResult result = await r.ResolvePtrAsync(IPAddress.Parse("8.8.8.8")); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); Assert.NotEmpty(result.Records); Assert.False(string.IsNullOrEmpty(result.Records[0].Name)); @@ -259,8 +260,9 @@ public async Task DnsResolver_CustomServer_NonStandardPort_ThrowsPlatformNotSupp [OuterLoop] public async Task ResolvePtr_IPv6Address_DoesNotThrow() { + using DnsResolver r = new DnsResolver(); // Google public DNS IPv6 — call shouldn't throw, even if no PTR record exists. - DnsResult result = await Dns.ResolvePtrAsync(IPAddress.Parse("2001:4860:4860::8888")); + DnsResult result = await r.ResolvePtrAsync(IPAddress.Parse("2001:4860:4860::8888")); Assert.True(result.ResponseCode == DnsResponseCode.NoError || result.ResponseCode == DnsResponseCode.NxDomain); } } From 6a9f9864e3bf1ea2912825aa384f4e7db6796ba1 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Wed, 17 Jun 2026 14:56:44 +0200 Subject: [PATCH 13/19] More code review feedback --- .../System.Net.NameResolution/src/System/Net/DnsResolver.cs | 2 +- .../src/System/Net/DnsResolverPal.Windows.cs | 2 +- .../tests/FunctionalTests/DnsResolverTest.cs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs index cc1b63e051b3ef..91475541c65a06 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs @@ -447,7 +447,7 @@ internal static unsafe string BuildArpaName(IPAddress address) address.TryWriteBytes(bytes, out _); Span chars = stackalloc char[16 * 4]; int pos = 0; - for (int i = 15; i >= 0; i--) + for (int i = bytes.Length - 1; i >= 0; i--) { byte b = bytes[i]; chars[pos++] = ToHex(b & 0xF); diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs index 42e51902629f34..da4df77095ed4b 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs @@ -248,7 +248,7 @@ private static DnsResult MergeAddressResults(DnsResult 0 || b.Records.Count > 0) { AddressRecord[] merged = [.. a.Records, .. b.Records]; - TimeSpan mergedTtl = a.NegativeCacheTtl > b.NegativeCacheTtl ? a.NegativeCacheTtl : b.NegativeCacheTtl; + TimeSpan mergedTtl = Math.Min(a.NegativeCacheTtl, b.NegativeCacheTtl); return new DnsResult(DnsResponseCode.NoError, merged, mergedTtl); } diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs index c7ac8895b3afca..372858867a3974 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs @@ -92,7 +92,9 @@ public async Task DnsResolver_DisposeAsync_ThrowsOnUse() await Assert.ThrowsAsync(() => r.ResolveAddressesAsync(TestHost)); } - [Fact] + // ---- Windows network tests (require outbound DNS) ---- + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] public async Task DnsResolver_PreCanceledToken_ReturnsCanceled() { using DnsResolver r = new DnsResolver(); @@ -101,8 +103,6 @@ public async Task DnsResolver_PreCanceledToken_ReturnsCanceled() await Assert.ThrowsAsync(() => r.ResolveAddressesAsync(TestHost, cts.Token)); } - // ---- Windows network tests (require outbound DNS) ---- - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] [OuterLoop] public async Task ResolveAddresses_KnownName_ReturnsRecords() From 023ff8bdb25eb034e377a29ad63cf5f6a2d79d3c Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Wed, 17 Jun 2026 15:15:43 +0200 Subject: [PATCH 14/19] Reduce unsafe code in Windows DNS PAL/interop layer Replace pointer-based record and sockaddr handling with safe Span and Marshal-based reads where possible: - TryParseAddress now reads DNS_A_DATA/DNS_AAAA_DATA via Marshal.PtrToStructure instead of byte* spans; DNS_AAAA_DATA uses an InlineArray field, removing the fixed buffer (and unsafe) from the interop struct. - BuildAddrArray populates the DNS_ADDR_ARRAY through a Span using BinaryPrimitives; WriteSockAddr takes Span instead of byte*. - Drop the unnecessary 'unsafe' modifier from PtrToString. Also fix a pre-existing compile break in MergeAddressResults (Math.Min has no TimeSpan overload). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Windows/Dnsapi/Interop.DnsTypes.cs | 10 +++- .../src/System/Net/DnsResolverPal.Windows.cs | 51 ++++++++++++------- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsTypes.cs b/src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsTypes.cs index db3519b1c5ef8b..08646c54ef3d21 100644 --- a/src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsTypes.cs +++ b/src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsTypes.cs @@ -132,9 +132,15 @@ internal struct DNS_A_DATA } [StructLayout(LayoutKind.Sequential)] - internal unsafe struct DNS_AAAA_DATA + internal struct DNS_AAAA_DATA { - public fixed byte Ip6Address[16]; + public Ip6AddressBytes Ip6Address; + } + + [System.Runtime.CompilerServices.InlineArray(16)] + internal struct Ip6AddressBytes + { + private byte _element0; } [StructLayout(LayoutKind.Sequential)] diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs index da4df77095ed4b..1b8c1fb7809350 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs @@ -248,7 +248,7 @@ private static DnsResult MergeAddressResults(DnsResult 0 || b.Records.Count > 0) { AddressRecord[] merged = [.. a.Records, .. b.Records]; - TimeSpan mergedTtl = Math.Min(a.NegativeCacheTtl, b.NegativeCacheTtl); + TimeSpan mergedTtl = a.NegativeCacheTtl < b.NegativeCacheTtl ? a.NegativeCacheTtl : b.NegativeCacheTtl; return new DnsResult(DnsResponseCode.NoError, merged, mergedTtl); } @@ -259,11 +259,23 @@ private static DnsResult MergeAddressResults(DnsResult(chosenRc, null, negTtl); } - private static unsafe bool TryParseAddress(ushort recordType, IntPtr dataPtr, out IPAddress? address) + private static bool TryParseAddress(ushort recordType, IntPtr dataPtr, out IPAddress? address) { - if (recordType is Interop.Dnsapi.DNS_TYPE_A or Interop.Dnsapi.DNS_TYPE_AAAA) + if (recordType == Interop.Dnsapi.DNS_TYPE_A) { - address = new IPAddress(new ReadOnlySpan((byte*)dataPtr, recordType == Interop.Dnsapi.DNS_TYPE_A ? 4 : 16)); + // DNS_A_DATA holds the IPv4 address as a uint already in network byte + // order, which is exactly the layout the IPAddress(long) ctor expects. + Interop.Dnsapi.DNS_A_DATA data = Marshal.PtrToStructure(dataPtr); + address = new IPAddress((long)data.IpAddress); + return true; + } + + if (recordType == Interop.Dnsapi.DNS_TYPE_AAAA) + { + // DNS_AAAA_DATA holds the 16 raw IPv6 address bytes; the InlineArray + // field exposes them as a span without any pointer arithmetic. + Interop.Dnsapi.DNS_AAAA_DATA data = Marshal.PtrToStructure(dataPtr); + address = new IPAddress((ReadOnlySpan)data.Ip6Address); return true; } @@ -313,7 +325,7 @@ private static Task DnsQueryEx(IList servers, boo return Task.FromResult(DnsQueryExSync(servers, name, queryType, cancellationToken)); } - private static unsafe string? PtrToString(IntPtr p) => + private static string? PtrToString(IntPtr p) => p == IntPtr.Zero ? null : Marshal.PtrToStringUni(p); // ---- Asynchronous DnsQueryEx state machine ---- @@ -637,24 +649,25 @@ private static unsafe void BuildAddrArray(IList servers, out IntPtr int totalSize = checked(headerSize + addrSize * count); arrayPtr = Marshal.AllocHGlobal(totalSize); - NativeMemory.Clear((void*)arrayPtr, (nuint)totalSize); - Interop.Dnsapi.DNS_ADDR_ARRAY* arr = (Interop.Dnsapi.DNS_ADDR_ARRAY*)arrayPtr; - arr->MaxCount = (uint)count; - arr->AddrCount = (uint)count; - arr->Family = (ushort)(family == AddressFamily.InterNetwork ? Interop.Dnsapi.AF_INET : Interop.Dnsapi.AF_INET6); + // Wrap the unmanaged buffer in a span and populate it without pointer arithmetic. + Span buffer = new Span((void*)arrayPtr, totalSize); + buffer.Clear(); + + ref Interop.Dnsapi.DNS_ADDR_ARRAY arr = ref MemoryMarshal.AsRef(buffer); + arr.MaxCount = (uint)count; + arr.AddrCount = (uint)count; + arr.Family = (ushort)(family == AddressFamily.InterNetwork ? Interop.Dnsapi.AF_INET : Interop.Dnsapi.AF_INET6); - byte* addrBase = (byte*)arrayPtr + headerSize; for (int i = 0; i < count; i++) { - byte* sa = addrBase + (i * addrSize); - WriteSockAddr(sa, servers[i].Address); + WriteSockAddr(buffer.Slice(headerSize + (i * addrSize), addrSize), servers[i].Address); } } // 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, IPAddress address) + private static void WriteSockAddr(Span dest, IPAddress address) { // DnsQueryEx always queries DNS servers on port 53 and requires the sockaddr // port field to be left as 0. Supplying a non-zero port (even 53) is rejected @@ -663,21 +676,21 @@ private static unsafe void WriteSockAddr(byte* dest, IPAddress address) if (address.AddressFamily == AddressFamily.InterNetwork) { // sockaddr_in: ushort family, ushort port (net order), uint addr, 8 bytes zero - *(ushort*)(dest + 0) = Interop.Dnsapi.AF_INET; + BinaryPrimitives.WriteUInt16LittleEndian(dest, Interop.Dnsapi.AF_INET); // dest[2..3] (port) left zero - address.TryWriteBytes(new Span(dest + 4, 4), out _); + address.TryWriteBytes(dest.Slice(4, 4), out _); // dest[8..15] left zero } else if (address.AddressFamily == AddressFamily.InterNetworkV6) { // sockaddr_in6: ushort family, ushort port, uint flowinfo, 16-byte addr, uint scope_id - *(ushort*)(dest + 0) = Interop.Dnsapi.AF_INET6; + BinaryPrimitives.WriteUInt16LittleEndian(dest, Interop.Dnsapi.AF_INET6); // dest[2..3] (port) left zero // flowinfo (dest[4..7]) left zero - address.TryWriteBytes(new Span(dest + 8, 16), out _); + address.TryWriteBytes(dest.Slice(8, 16), out _); // scope_id (dest[24..27]) is in host byte order; this code is Windows-only // (always little-endian), so write it as little-endian. - BinaryPrimitives.WriteUInt32LittleEndian(new Span(dest + 24, 4), (uint)address.ScopeId); + BinaryPrimitives.WriteUInt32LittleEndian(dest.Slice(24, 4), (uint)address.ScopeId); } else { From 42478610b27a2497384ec509a5d7e3f16f9c6a9a Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Wed, 17 Jun 2026 17:05:09 +0200 Subject: [PATCH 15/19] More minor changes --- .../src/System/Net/DnsResolver.cs | 13 +++- .../src/System/Net/DnsResolverPal.Windows.cs | 60 ++++++++----------- 2 files changed, 34 insertions(+), 39 deletions(-) diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs index 91475541c65a06..31b764428f2362 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs @@ -35,9 +35,16 @@ public DnsResolver() : this(new DnsResolverOptions()) { } public DnsResolver(DnsResolverOptions options) { ArgumentNullException.ThrowIfNull(options); - // Capture a snapshot of the configured servers so later mutations of the - // options instance don't affect resolutions performed by this resolver. - _servers = [.. options.Servers]; + // Capture a defensive snapshot of the configured servers. IPEndPoint is + // mutable, so clone each entry to ensure later mutations of the options + // instance (or the endpoints it holds) don't affect this resolver. + IList servers = options.Servers; + _servers = new IPEndPoint[servers.Count]; + for (int i = 0; i < _servers.Length; i++) + { + IPEndPoint server = servers[i]; + _servers[i] = new IPEndPoint(server.Address, server.Port); + } } /// diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs index 1b8c1fb7809350..18d7e35958dbf9 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs @@ -1,10 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Buffers.Binary; using System.Collections.Generic; using System.Diagnostics; using System.Net.Sockets; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -137,7 +137,7 @@ private static DnsResult ParseAddresses(DnsQueryRawResult raw, us uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; if (hdr.wType == qtype && section == Interop.Dnsapi.DNSREC_ANSWER) { - IntPtr dataPtr = cur + Marshal.SizeOf(); + IntPtr dataPtr = cur + Unsafe.SizeOf(); if (TryParseAddress(hdr.wType, dataPtr, out IPAddress? address)) { records.Add(new AddressRecord(address!, TimeSpan.FromSeconds(hdr.dwTtl))); @@ -167,7 +167,7 @@ private static DnsResult ParseSrv(DnsQueryRawResult raw) uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; if (hdr.wType == Interop.Dnsapi.DNS_TYPE_SRV && section == Interop.Dnsapi.DNSREC_ANSWER) { - IntPtr dataPtr = cur + Marshal.SizeOf(); + IntPtr dataPtr = cur + Unsafe.SizeOf(); Interop.Dnsapi.DNS_SRV_DATA data = Marshal.PtrToStructure(dataPtr); string target = PtrToString(data.pNameTarget) ?? string.Empty; List? attached = null; @@ -194,7 +194,7 @@ private static DnsResult ParseTxt(DnsQueryRawResult raw) uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; if (hdr.wType == Interop.Dnsapi.DNS_TYPE_TEXT && section == Interop.Dnsapi.DNSREC_ANSWER) { - IntPtr dataPtr = cur + Marshal.SizeOf(); + IntPtr dataPtr = cur + Unsafe.SizeOf(); // DNS_TXT_DATA: uint dwStringCount; followed by array of PCWSTR. uint count = (uint)Marshal.ReadInt32(dataPtr); int ptrSize = IntPtr.Size; @@ -234,7 +234,7 @@ private static DnsResult ParseSimple(DnsQueryRawResult raw, us uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; if (hdr.wType == qtype && section == Interop.Dnsapi.DNSREC_ANSWER) { - IntPtr dataPtr = cur + Marshal.SizeOf(); + IntPtr dataPtr = cur + Unsafe.SizeOf(); records.Add(selector(hdr, dataPtr)); } cur = hdr.pNext; @@ -292,7 +292,7 @@ private static void ParseAdditionalAddresses(IntPtr head, ref Dictionary(); + IntPtr dataPtr = cur + Unsafe.SizeOf(); if (TryParseAddress(hdr.wType, dataPtr, out IPAddress? address)) { string name = PtrToString(hdr.pName) ?? string.Empty; @@ -328,6 +328,14 @@ private static Task DnsQueryEx(IList servers, boo private static string? PtrToString(IntPtr p) => p == IntPtr.Zero ? null : Marshal.PtrToStringUni(p); + // Reinterprets native memory (records and result structures returned by + // DnsQueryEx) as a managed read-only reference. The structures are blittable + // and remain valid until the record list / result is freed, so this avoids the + // marshalling copy of Marshal.PtrToStructure. The single pointer cast is + // confined here so callers stay free of the 'unsafe' keyword. + private static unsafe ref readonly T AsStruct(IntPtr ptr) where T : unmanaged => + ref Unsafe.AsRef((void*)ptr); + // ---- Asynchronous DnsQueryEx state machine ---- // Cached callback so we don't allocate a new delegate per query. @@ -661,41 +669,21 @@ private static unsafe void BuildAddrArray(IList servers, out IntPtr for (int i = 0; i < count; i++) { - WriteSockAddr(buffer.Slice(headerSize + (i * addrSize), addrSize), servers[i].Address); + WriteSockAddr(buffer.Slice(headerSize + (i * addrSize), addrSize), servers[i]); } } // 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 void WriteSockAddr(Span dest, IPAddress address) + private static void WriteSockAddr(Span dest, IPEndPoint endpoint) { - // DnsQueryEx always queries DNS servers on port 53 and requires the sockaddr - // port field to be left as 0. Supplying a non-zero port (even 53) is rejected - // with ERROR_INVALID_PARAMETER. Non-default ports are validated and rejected - // earlier, so the port is always written as 0 here. - if (address.AddressFamily == AddressFamily.InterNetwork) - { - // sockaddr_in: ushort family, ushort port (net order), uint addr, 8 bytes zero - BinaryPrimitives.WriteUInt16LittleEndian(dest, Interop.Dnsapi.AF_INET); - // dest[2..3] (port) left zero - address.TryWriteBytes(dest.Slice(4, 4), out _); - // dest[8..15] left zero - } - else if (address.AddressFamily == AddressFamily.InterNetworkV6) - { - // sockaddr_in6: ushort family, ushort port, uint flowinfo, 16-byte addr, uint scope_id - BinaryPrimitives.WriteUInt16LittleEndian(dest, Interop.Dnsapi.AF_INET6); - // dest[2..3] (port) left zero - // flowinfo (dest[4..7]) left zero - address.TryWriteBytes(dest.Slice(8, 16), out _); - // scope_id (dest[24..27]) is in host byte order; this code is Windows-only - // (always little-endian), so write it as little-endian. - BinaryPrimitives.WriteUInt32LittleEndian(dest.Slice(24, 4), (uint)address.ScopeId); - } - else - { - throw new ArgumentException(SR.net_invalid_ip_addr); - } + // IPEndPoint.Serialize() yields the platform SOCKADDR layout (family, port, + // address, and for IPv6 the flow info and scope id) via SocketAddressPal, so + // there's no need to lay out the bytes by hand. DnsQueryEx always queries DNS + // servers on port 53 and requires the sockaddr port field to be left as 0; + // serializing a port-0 endpoint satisfies that. + SocketAddress socketAddress = endpoint.Serialize(); + socketAddress.Buffer.Span.Slice(0, socketAddress.Size).CopyTo(dest); } private static DnsResponseCode MapWindowsErrorToResponseCode(int status) => @@ -720,7 +708,7 @@ private static TimeSpan ExtractNegativeCacheTtl(IntPtr head) uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; if (hdr.wType == Interop.Dnsapi.DNS_TYPE_SOA && section == Interop.Dnsapi.DNSREC_AUTHORITY) { - IntPtr dataPtr = cur + Marshal.SizeOf(); + IntPtr dataPtr = cur + Unsafe.SizeOf(); Interop.Dnsapi.DNS_SOA_DATA soa = Marshal.PtrToStructure(dataPtr); // RFC 2308 §5: negative cache TTL = min(SOA TTL, SOA MINIMUM) uint negTtl = Math.Min(hdr.dwTtl, soa.dwDefaultTtl); From 3af512abadcffcec541a0af17bc16eb48ee44c21 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Thu, 18 Jun 2026 11:18:51 +0200 Subject: [PATCH 16/19] Address PR review feedback: thread-safety and server validation - Dns.DefaultResolver: use LazyInitializer.EnsureInitialized for thread-safe one-time initialization instead of non-atomic ??=. - DnsResolver ctor: reject null entries in the server snapshot with an ArgumentException pointing at the public Servers property. - BuildAddrArray: surface mixed-address-family ArgumentException with the public-facing 'Servers' parameter name instead of the internal 'servers'. - LoopbackDnsServer: use ConcurrentDictionary for _responses to avoid races between test-thread mutations and listener-thread reads. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../System.Net.NameResolution/src/Resources/Strings.resx | 3 +++ .../System.Net.NameResolution/src/System/Net/Dns.Resolve.cs | 2 +- .../System.Net.NameResolution/src/System/Net/DnsResolver.cs | 4 ++++ .../src/System/Net/DnsResolverPal.Windows.cs | 2 +- .../tests/FunctionalTests/LoopbackDnsServer.cs | 3 ++- 5 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Net.NameResolution/src/Resources/Strings.resx b/src/libraries/System.Net.NameResolution/src/Resources/Strings.resx index 44874623ad8cf6..b5db944dc9ce56 100644 --- a/src/libraries/System.Net.NameResolution/src/Resources/Strings.resx +++ b/src/libraries/System.Net.NameResolution/src/Resources/Strings.resx @@ -81,4 +81,7 @@ All DNS server endpoints must belong to the same address family. + + The DNS server list must not contain null entries. + \ No newline at end of file diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/Dns.Resolve.cs b/src/libraries/System.Net.NameResolution/src/System/Net/Dns.Resolve.cs index 6a5943d681e859..03055bba782e26 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/Dns.Resolve.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/Dns.Resolve.cs @@ -14,7 +14,7 @@ public static partial class Dns private static DnsResolver? s_defaultResolver; private static DnsResolver DefaultResolver => - s_defaultResolver ??= new DnsResolver(); + LazyInitializer.EnsureInitialized(ref s_defaultResolver, static () => new DnsResolver()); /// /// Resolves the IPv4 (A) and IPv6 (AAAA) addresses for the specified host name using the system-configured DNS servers. diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs index 31b764428f2362..dcf102ca4711a4 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs @@ -43,6 +43,10 @@ public DnsResolver(DnsResolverOptions options) for (int i = 0; i < _servers.Length; i++) { IPEndPoint server = servers[i]; + if (server is null) + { + throw new ArgumentException(SR.net_dns_servers_contains_null, $"{nameof(options)}.{nameof(DnsResolverOptions.Servers)}"); + } _servers[i] = new IPEndPoint(server.Address, server.Port); } } diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs index 18d7e35958dbf9..1b1ae56699e362 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs @@ -648,7 +648,7 @@ private static unsafe void BuildAddrArray(IList servers, out IntPtr { if (servers[i].AddressFamily != family) { - throw new ArgumentException(SR.net_dns_mixed_address_families, nameof(servers)); + throw new ArgumentException(SR.net_dns_mixed_address_families, nameof(DnsResolverOptions.Servers)); } } diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/LoopbackDnsServer.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/LoopbackDnsServer.cs index e871a80f76ec99..004a0600759496 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/LoopbackDnsServer.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/LoopbackDnsServer.cs @@ -3,6 +3,7 @@ using System.Buffers.Binary; using System.Diagnostics; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Net.Sockets; using System.Text; @@ -34,7 +35,7 @@ internal sealed class LoopbackDnsServer : IAsyncDisposable private readonly CancellationTokenSource _cts = new(); private readonly Task _udpListenTask; private readonly Task _tcpListenTask; - private readonly Dictionary<(string Name, DnsRecordType Type), ResponseBuilder> _responses = new(); + private readonly ConcurrentDictionary<(string Name, DnsRecordType Type), ResponseBuilder> _responses = new(); private int _requestCount; public IPEndPoint EndPoint { get; } From 14b6592c369563ca612cc92576efeef771999bb6 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Thu, 18 Jun 2026 13:56:35 +0200 Subject: [PATCH 17/19] Add synchronous API coverage to DnsResolverTest via bool async theories Convert the Windows network tests and the DnsQueryEx synchronous-completion regression test to [ConditionalTheory] parameterized over bool async, using sync/async dispatch helpers mirroring DnsResolverLoopbackTest, so the synchronous Resolve* overloads are exercised alongside the async ones. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/FunctionalTests/DnsResolverTest.cs | 143 ++++++++++++++---- 1 file changed, 110 insertions(+), 33 deletions(-) diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs index 372858867a3974..706171b078aa68 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs @@ -92,6 +92,31 @@ public async Task DnsResolver_DisposeAsync_ThrowsOnUse() await Assert.ThrowsAsync(() => r.ResolveAddressesAsync(TestHost)); } + // ---- Sync/async dispatch helpers ---- + // The synchronous overloads execute inline on the calling thread; the results + // are wrapped in a completed Task so each test can await a single helper. + + private static async Task> ResolveAddresses(bool async, DnsResolver resolver, string name, AddressFamily addressFamily = AddressFamily.Unspecified) + => async ? await resolver.ResolveAddressesAsync(name, addressFamily) : resolver.ResolveAddresses(name, addressFamily); + + private static async Task> ResolveMx(bool async, DnsResolver resolver, string name) + => async ? await resolver.ResolveMxAsync(name) : resolver.ResolveMx(name); + + private static async Task> ResolveTxt(bool async, DnsResolver resolver, string name) + => async ? await resolver.ResolveTxtAsync(name) : resolver.ResolveTxt(name); + + private static async Task> ResolveCName(bool async, DnsResolver resolver, string name) + => async ? await resolver.ResolveCNameAsync(name) : resolver.ResolveCName(name); + + private static async Task> ResolveNs(bool async, DnsResolver resolver, string name) + => async ? await resolver.ResolveNsAsync(name) : resolver.ResolveNs(name); + + private static async Task> ResolvePtr(bool async, DnsResolver resolver, IPAddress address) + => async ? await resolver.ResolvePtrAsync(address) : resolver.ResolvePtr(address); + + private static async Task> Static_ResolveAddresses(bool async, string name) + => async ? await Dns.ResolveAddressesAsync(name) : Dns.ResolveAddresses(name); + // ---- Windows network tests (require outbound DNS) ---- [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] @@ -103,12 +128,44 @@ public async Task DnsResolver_PreCanceledToken_ReturnsCanceled() await Assert.ThrowsAsync(() => r.ResolveAddressesAsync(TestHost, cts.Token)); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + // Regression test for the Windows 10 DnsQueryEx bug where an asynchronous query + // that the OS can satisfy synchronously (for example "localhost") returns + // ERROR_SUCCESS inline and never invokes the registered completion callback. + // If the implementation waited for that callback it would hang forever; the PAL + // must instead detect the synchronous completion (any status other than + // DNS_REQUEST_PENDING) and surface the result directly. + // See https://dblohm7.ca/blog/2022/05/06/dnsqueryex-needs-love/. + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveAddresses_SynchronouslyCompletingQuery_DoesNotHang(bool async) + { + using DnsResolver r = new DnsResolver(); + + // "localhost" can be answered without any network round-trip, which is what + // triggers the synchronous-completion path inside DnsQueryEx. A short timeout + // turns the "callback never fires" hang into a test failure rather than letting + // the run stall. + Task> task = ResolveAddresses(async, r, "localhost"); + DnsResult result = await task.WaitAsync(TimeSpan.FromSeconds(30)); + + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + // DnsQueryEx performs a pure-DNS lookup, so "localhost" may legitimately yield + // no records (NODATA); if any are returned they must be loopback addresses. + foreach (AddressRecord rec in result.Records) + { + Assert.True(IPAddress.IsLoopback(rec.Address), $"Expected a loopback address but got {rec.Address}."); + } + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] [OuterLoop] - public async Task ResolveAddresses_KnownName_ReturnsRecords() + public async Task ResolveAddresses_KnownName_ReturnsRecords(bool async) { using DnsResolver r = new DnsResolver(); - DnsResult result = await r.ResolveAddressesAsync(TestHost); + DnsResult result = await ResolveAddresses(async, r, TestHost); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); Assert.NotEmpty(result.Records); foreach (AddressRecord rec in result.Records) @@ -118,12 +175,14 @@ public async Task ResolveAddresses_KnownName_ReturnsRecords() } } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] [OuterLoop] - public async Task ResolveAddresses_IPv4Only_ReturnsOnlyIPv4() + public async Task ResolveAddresses_IPv4Only_ReturnsOnlyIPv4(bool async) { using DnsResolver r = new DnsResolver(); - DnsResult result = await r.ResolveAddressesAsync(TestHost, AddressFamily.InterNetwork); + DnsResult result = await ResolveAddresses(async, r, TestHost, AddressFamily.InterNetwork); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); foreach (AddressRecord rec in result.Records) { @@ -131,22 +190,26 @@ public async Task ResolveAddresses_IPv4Only_ReturnsOnlyIPv4() } } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] [OuterLoop] - public async Task ResolveAddresses_NonExistent_ReturnsNxDomain() + public async Task ResolveAddresses_NonExistent_ReturnsNxDomain(bool async) { using DnsResolver r = new DnsResolver(); - DnsResult result = await r.ResolveAddressesAsync(NonExistentHost); + DnsResult result = await ResolveAddresses(async, r, NonExistentHost); Assert.Equal(DnsResponseCode.NxDomain, result.ResponseCode); Assert.Empty(result.Records); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] [OuterLoop] - public async Task ResolveMx_KnownName_ReturnsRecords() + public async Task ResolveMx_KnownName_ReturnsRecords(bool async) { using DnsResolver r = new DnsResolver(); - DnsResult result = await r.ResolveMxAsync(TestMxHost); + DnsResult result = await ResolveMx(async, r, TestMxHost); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); Assert.NotEmpty(result.Records); foreach (MxRecord rec in result.Records) @@ -155,12 +218,14 @@ public async Task ResolveMx_KnownName_ReturnsRecords() } } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] [OuterLoop] - public async Task ResolveTxt_KnownName_ReturnsRecords() + public async Task ResolveTxt_KnownName_ReturnsRecords(bool async) { using DnsResolver r = new DnsResolver(); - DnsResult result = await r.ResolveTxtAsync(TestTxtHost); + DnsResult result = await ResolveTxt(async, r, TestTxtHost); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); Assert.NotEmpty(result.Records); foreach (TxtRecord rec in result.Records) @@ -169,12 +234,14 @@ public async Task ResolveTxt_KnownName_ReturnsRecords() } } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] [OuterLoop] - public async Task ResolveCName_KnownName_ReturnsRecord() + public async Task ResolveCName_KnownName_ReturnsRecord(bool async) { using DnsResolver r = new DnsResolver(); - DnsResult result = await r.ResolveCNameAsync(TestCNameHost); + DnsResult result = await ResolveCName(async, r, TestCNameHost); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); // CNAME may or may not exist for the target; at minimum the call should succeed. if (result.Records.Count > 0) @@ -183,12 +250,14 @@ public async Task ResolveCName_KnownName_ReturnsRecord() } } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] [OuterLoop] - public async Task ResolveNs_KnownName_ReturnsRecords() + public async Task ResolveNs_KnownName_ReturnsRecords(bool async) { using DnsResolver r = new DnsResolver(); - DnsResult result = await r.ResolveNsAsync(TestNsHost); + DnsResult result = await ResolveNs(async, r, TestNsHost); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); Assert.NotEmpty(result.Records); foreach (NsRecord rec in result.Records) @@ -197,29 +266,35 @@ public async Task ResolveNs_KnownName_ReturnsRecords() } } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] [OuterLoop] - public async Task ResolvePtr_ByIPAddress_ReturnsRecord() + public async Task ResolvePtr_ByIPAddress_ReturnsRecord(bool async) { using DnsResolver r = new DnsResolver(); - DnsResult result = await r.ResolvePtrAsync(IPAddress.Parse("8.8.8.8")); + DnsResult result = await ResolvePtr(async, r, IPAddress.Parse("8.8.8.8")); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); Assert.NotEmpty(result.Records); Assert.False(string.IsNullOrEmpty(result.Records[0].Name)); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] [OuterLoop] - public async Task Static_Dns_ResolveAddressesAsync_Works() + public async Task Static_Dns_ResolveAddresses_Works(bool async) { - DnsResult result = await Dns.ResolveAddressesAsync(TestHost); + DnsResult result = await Static_ResolveAddresses(async, TestHost); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); Assert.NotEmpty(result.Records); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] [OuterLoop] - public async Task DnsResolver_CustomServer_Port53_Works() + public async Task DnsResolver_CustomServer_Port53_Works(bool async) { IPAddress? dnsAddress = System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces() .SelectMany(ni => ni.GetIPProperties().DnsAddresses) @@ -236,7 +311,7 @@ public async Task DnsResolver_CustomServer_Port53_Works() Servers = { new IPEndPoint(dnsAddress, 53) } }; using DnsResolver r = new DnsResolver(opts); - DnsResult result = await r.ResolveAddressesAsync(TestHost); + DnsResult result = await ResolveAddresses(async, r, TestHost); Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); Assert.NotEmpty(result.Records); } @@ -256,13 +331,15 @@ public async Task DnsResolver_CustomServer_NonStandardPort_ThrowsPlatformNotSupp // ---- Reverse-arpa name building (covers both IPv4 and IPv6 paths used by ResolvePtr(IPAddress)) ---- - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] [OuterLoop] - public async Task ResolvePtr_IPv6Address_DoesNotThrow() + public async Task ResolvePtr_IPv6Address_DoesNotThrow(bool async) { using DnsResolver r = new DnsResolver(); // Google public DNS IPv6 — call shouldn't throw, even if no PTR record exists. - DnsResult result = await r.ResolvePtrAsync(IPAddress.Parse("2001:4860:4860::8888")); + DnsResult result = await ResolvePtr(async, r, IPAddress.Parse("2001:4860:4860::8888")); Assert.True(result.ResponseCode == DnsResponseCode.NoError || result.ResponseCode == DnsResponseCode.NxDomain); } } From ed8096c7c465f78f4bf53a2c3c83bab49f86a70a Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Thu, 18 Jun 2026 14:24:31 +0200 Subject: [PATCH 18/19] Address PR review feedback: port normalization, skip handling, comments - WriteSockAddr now takes an IPAddress and always serializes a port-0 SOCKADDR, so custom-server endpoints with port 53 are normalized to 0 as DnsQueryEx requires (avoids serializing a non-default port). Taking IPAddress also avoids reading from the caller's mutable IPEndPoint. - Clarify the ValidateServerPorts comment: 0 and 53 are accepted and normalized to 0; any other port is rejected. - Fix the DNS_RECORD_HEADER comment: the Data union offset is 24 bytes on 32-bit and 32 bytes on 64-bit (two header pointers), so callers use Unsafe.SizeOf. - Loopback test fixture: start the server lazily on first access instead of in the IClassFixture constructor, and convert the tests to ConditionalFact/ ConditionalTheory so LoopbackDnsServer.Start()'s SkipTestException is honored (port 53 unavailable now skips rather than failing). Also stop assigning null to the server field. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Windows/Dnsapi/Interop.DnsTypes.cs | 7 +- .../src/System/Net/DnsResolverPal.Windows.cs | 28 ++++---- .../DnsResolverLoopbackTest.cs | 64 +++++++++---------- 3 files changed, 51 insertions(+), 48 deletions(-) diff --git a/src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsTypes.cs b/src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsTypes.cs index 08646c54ef3d21..c656df7e6c6b13 100644 --- a/src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsTypes.cs +++ b/src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsTypes.cs @@ -100,9 +100,10 @@ internal unsafe struct DNS_CUSTOM_SERVER // ---- DNS_RECORD (variable layout: header + Data union) ---- // We declare the fixed header layout and read the data area as a byte blob, - // re-interpreting per record type. The Data field begins at offset 24 on 32-bit - // and at offset 24 on 64-bit pointer layouts as documented; we use a - // Sequential struct with explicit Next/pName pointers. + // re-interpreting per record type. The Data union follows the header; because the + // header contains two pointers, its size (and therefore the Data offset) depends on + // the pointer width - 24 bytes on 32-bit and 32 bytes on 64-bit. Callers must use + // Unsafe.SizeOf() rather than a hard-coded offset. [StructLayout(LayoutKind.Sequential)] internal struct DNS_RECORD_HEADER { diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs index 1b1ae56699e362..b3d5817185ab60 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs @@ -553,11 +553,12 @@ private static void QueryCompletionCallback(IntPtr pQueryContext, IntPtr pQueryR } } - // DnsQueryEx only supports DNS servers reachable on the standard port 53. - // The sockaddr port field passed to the API must be 0 (the API always - // queries port 53); supplying any non-zero port - even 53 itself - results - // in ERROR_INVALID_PARAMETER. We therefore reject any server endpoint that - // requests a non-default port, since it cannot be honored on Windows. + // DnsQueryEx always queries DNS servers on the standard port 53 and requires the + // sockaddr port field passed to the API to be left as 0; supplying any other + // non-zero port results in ERROR_INVALID_PARAMETER. We accept either 0 ("use the + // default port") or 53 (the port DnsQueryEx will actually use) and normalize both + // to 0 when building the native server list (see WriteSockAddr). Any other port + // cannot be honored on Windows and is rejected here. private static void ValidateServerPorts(IList servers) { foreach (IPEndPoint ep in servers) @@ -669,20 +670,21 @@ private static unsafe void BuildAddrArray(IList servers, out IntPtr for (int i = 0; i < count; i++) { - WriteSockAddr(buffer.Slice(headerSize + (i * addrSize), addrSize), servers[i]); + WriteSockAddr(buffer.Slice(headerSize + (i * addrSize), addrSize), servers[i].Address); } } // 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 void WriteSockAddr(Span dest, IPEndPoint endpoint) + private static void WriteSockAddr(Span dest, IPAddress address) { - // IPEndPoint.Serialize() yields the platform SOCKADDR layout (family, port, - // address, and for IPv6 the flow info and scope id) via SocketAddressPal, so - // there's no need to lay out the bytes by hand. DnsQueryEx always queries DNS - // servers on port 53 and requires the sockaddr port field to be left as 0; - // serializing a port-0 endpoint satisfies that. - SocketAddress socketAddress = endpoint.Serialize(); + // DnsQueryEx always queries DNS servers on port 53 and requires the sockaddr + // port field to be left as 0, so we build the SOCKADDR from a port-0 endpoint. + // Taking an IPAddress (rather than the caller's mutable IPEndPoint) also ensures + // we can never accidentally serialize a non-default port. SocketAddressPal lays + // out the platform SOCKADDR (family, port, address, and for IPv6 the flow info + // and scope id), so there's no need to write the bytes by hand. + SocketAddress socketAddress = new IPEndPoint(address, 0).Serialize(); socketAddress.Buffer.Span.Slice(0, socketAddress.Size).CopyTo(dest); } diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs index f0effd738aae1a..6651a1984afe18 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs @@ -11,23 +11,22 @@ namespace System.Net.NameResolution.Tests { - public class WindowsLoopbackServer : IAsyncDisposable + public sealed class WindowsLoopbackServer : IAsyncDisposable { - private LoopbackDnsServer _server; + private LoopbackDnsServer? _server; - public WindowsLoopbackServer() - { - _server = LoopbackDnsServer.Start(); - } - - internal LoopbackDnsServer Server => _server; + // Started lazily on first access (from within a test invocation) rather than in + // the constructor. LoopbackDnsServer.Start() throws SkipTestException when port 53 + // is unavailable; that is only honored by the ConditionalFact/ConditionalTheory + // runner, which wraps the test class constructor where Server is first accessed. + // Starting in this fixture constructor would instead surface as a hard failure. + internal LoopbackDnsServer Server => _server ??= LoopbackDnsServer.Start(); public async ValueTask DisposeAsync() { - if (_server != null) + if (_server is not null) { await _server.DisposeAsync(); - _server = null; } } } @@ -37,8 +36,10 @@ public async ValueTask DisposeAsync() // On Windows, DnsQueryEx only ever contacts custom DNS servers on the standard // port 53 (the sockaddr port field must be 0), so the loopback server binds port 53. // When that port is unavailable (e.g. a local DNS service is already running) the - // tests are skipped via SkipTestException rather than failing. Because the single - // machine-wide port 53 is shared, these tests run sequentially (see the collection). + // server's Start() throws SkipTestException; the tests therefore use + // ConditionalFact/ConditionalTheory so that skip is honored rather than surfacing as + // a failure. Because the single machine-wide port 53 is shared, these tests run + // sequentially (see the collection). // // Each behavioral test is parameterized over the synchronous and asynchronous APIs // so both code paths are exercised against the same loopback responses. @@ -47,7 +48,6 @@ public async ValueTask DisposeAsync() // OuterLoop tests in DnsResolverTest.cs cannot exercise deterministically. [OuterLoop("Binds the loopback DNS port 53 and issues real DnsQueryEx calls.")] [Collection(nameof(DisableParallelization))] - [PlatformSpecific(TestPlatforms.Windows)] public class DnsResolverLoopbackTest : IClassFixture { private static DnsResolver CreateResolver(LoopbackDnsServer server) @@ -95,7 +95,7 @@ private static async Task> ResolveNs(bool async, DnsResolver // ---- Address resolution ---- - [Theory] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] [InlineData(false)] [InlineData(true)] public async Task ResolveAddresses_Unspecified_ReturnsBothV4AndV6(bool async) @@ -112,7 +112,7 @@ public async Task ResolveAddresses_Unspecified_ReturnsBothV4AndV6(bool async) Assert.Contains(result.Records, a => a.Address.ToString() == "fd00::1"); } - [Theory] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] [InlineData(false)] [InlineData(true)] public async Task ResolveAddresses_IPv4Only_ReturnsOnlyV4(bool async) @@ -129,7 +129,7 @@ public async Task ResolveAddresses_IPv4Only_ReturnsOnlyV4(bool async) Assert.Equal("10.0.0.2", record.Address.ToString()); } - [Theory] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] [InlineData(false)] [InlineData(true)] public async Task ResolveAddresses_IPv6Only_ReturnsOnlyV6(bool async) @@ -145,7 +145,7 @@ public async Task ResolveAddresses_IPv6Only_ReturnsOnlyV6(bool async) Assert.Equal("fd00::1", record.Address.ToString()); } - [Theory] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] [InlineData(false)] [InlineData(true)] public async Task ResolveAddresses_AddressFamilyV4_QueriesOnlyA(bool async) @@ -160,7 +160,7 @@ public async Task ResolveAddresses_AddressFamilyV4_QueriesOnlyA(bool async) Assert.Equal("192.0.2.7", record.Address.ToString()); } - [Theory] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] [InlineData(false)] [InlineData(true)] public async Task ResolveAddresses_HasTtl(bool async) @@ -177,7 +177,7 @@ public async Task ResolveAddresses_HasTtl(bool async) $"Unexpected TTL: {record.Ttl}"); } - [Theory] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] [InlineData(false)] [InlineData(true)] public async Task ResolveAddresses_Nxdomain_ReturnsNxDomain(bool async) @@ -200,7 +200,7 @@ public async Task ResolveAddresses_Nxdomain_ReturnsNxDomain(bool async) $"Unexpected NegativeCacheTtl: {result.NegativeCacheTtl}"); } - [Theory] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] [InlineData(false)] [InlineData(true)] public async Task ResolveAddresses_NoData_ReturnsNoErrorWithEmptyRecords(bool async) @@ -222,7 +222,7 @@ public async Task ResolveAddresses_NoData_ReturnsNoErrorWithEmptyRecords(bool as $"Unexpected NegativeCacheTtl: {result.NegativeCacheTtl}"); } - [Theory] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] [InlineData(false)] [InlineData(true)] public async Task ResolveAddresses_NoData_And_Nxdomain_AreDistinguishable(bool async) @@ -256,7 +256,7 @@ public async Task ResolveAddresses_NoData_And_Nxdomain_AreDistinguishable(bool a // ---- SRV ---- - [Theory] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] [InlineData(false)] [InlineData(true)] public async Task ResolveSrv_ReturnsRecords(bool async) @@ -281,7 +281,7 @@ public async Task ResolveSrv_ReturnsRecords(bool async) Assert.Equal((ushort)20, s2.Priority); } - [Theory] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] [InlineData(false)] [InlineData(true)] public async Task ResolveSrv_IncludesAdditionalAddresses(bool async) @@ -306,7 +306,7 @@ public async Task ResolveSrv_IncludesAdditionalAddresses(bool async) Assert.Equal(2, s2.Addresses.Count); } - [Theory] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] [InlineData(false)] [InlineData(true)] public async Task ResolveSrv_NoAdditionalAddresses(bool async) @@ -324,7 +324,7 @@ public async Task ResolveSrv_NoAdditionalAddresses(bool async) // ---- MX / TXT / CNAME / PTR / NS ---- - [Theory] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] [InlineData(false)] [InlineData(true)] public async Task ResolveMx_ReturnsRecords(bool async) @@ -344,7 +344,7 @@ public async Task ResolveMx_ReturnsRecords(bool async) Assert.Single(result.Records, m => m.Exchange == "mail2.test" && m.Preference == 20); } - [Theory] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] [InlineData(false)] [InlineData(true)] public async Task ResolveTxt_ReturnsValues(bool async) @@ -362,7 +362,7 @@ public async Task ResolveTxt_ReturnsValues(bool async) Assert.Contains(result.Records, t => t.Values.Count == 2 && t.Values[0] == "part1" && t.Values[1] == "part2"); } - [Theory] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] [InlineData(false)] [InlineData(true)] public async Task ResolveCName_ReturnsCanonicalName(bool async) @@ -378,7 +378,7 @@ public async Task ResolveCName_ReturnsCanonicalName(bool async) Assert.Equal("canonical.test", record.CanonicalName); } - [Theory] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] [InlineData(false)] [InlineData(true)] public async Task ResolvePtr_ReturnsName(bool async) @@ -394,7 +394,7 @@ public async Task ResolvePtr_ReturnsName(bool async) Assert.Equal("host.test", record.Name); } - [Theory] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] [InlineData(false)] [InlineData(true)] public async Task ResolveNs_ReturnsRecords(bool async) @@ -414,7 +414,7 @@ public async Task ResolveNs_ReturnsRecords(bool async) // ---- Custom server endpoint handling ---- - [Theory] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] [InlineData(false)] [InlineData(true)] public async Task CustomServer_DefaultPortZero_IsAccepted(bool async) @@ -438,7 +438,7 @@ public async Task CustomServer_DefaultPortZero_IsAccepted(bool async) // ---- Cancellation while a query is in flight ---- - [Fact] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] public async Task ResolveAddresses_CancellationInFlight_Throws() { using SemaphoreSlim queryReceived = new(0, 1); @@ -469,7 +469,7 @@ public async Task ResolveAddresses_CancellationInFlight_Throws() // ---- Telemetry ---- - [Theory] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] [InlineData(false)] [InlineData(true)] public async Task ResolveAddresses_RecordsDurationMetric_CoversQueryTime(bool async) From 693bd6ebb9df8b140358be8fbb45a4149c6bae9b Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Thu, 25 Jun 2026 14:31:57 +0200 Subject: [PATCH 19/19] Address PR #129845 review feedback - MergeAddressResults: pick the minimum non-zero negative-cache TTL when merging A/AAAA results instead of the first positive one. - Use a dedicated SR string (net_dns_unsupported_address_family) for unsupported AddressFamily values in AddressFamilyToQueryType and BuildArpaName instead of the unrelated net_invalid_ip_addr message. - LoopbackDnsServer: wrap the fire-and-forget UDP request handler in a try/catch for expected teardown exceptions to avoid unobserved task exceptions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Resources/Strings.resx | 3 +++ .../src/System/Net/DnsResolver.cs | 2 +- .../src/System/Net/DnsResolverPal.Windows.cs | 20 +++++++++++++++++-- .../FunctionalTests/LoopbackDnsServer.cs | 16 ++++++++++----- 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Net.NameResolution/src/Resources/Strings.resx b/src/libraries/System.Net.NameResolution/src/Resources/Strings.resx index b5db944dc9ce56..5333f8ea48b72c 100644 --- a/src/libraries/System.Net.NameResolution/src/Resources/Strings.resx +++ b/src/libraries/System.Net.NameResolution/src/Resources/Strings.resx @@ -84,4 +84,7 @@ The DNS server list must not contain null entries. + + Only the InterNetwork and InterNetworkV6 address families are supported. + \ No newline at end of file diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs index dcf102ca4711a4..bcc2b116bc749a 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs @@ -472,7 +472,7 @@ internal static unsafe string BuildArpaName(IPAddress address) } else { - throw new ArgumentException(SR.net_invalid_ip_addr, nameof(address)); + throw new ArgumentException(SR.net_dns_unsupported_address_family, nameof(address)); } } } diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs index b3d5817185ab60..193e75ce5a6fc5 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs @@ -103,7 +103,7 @@ private static ushort AddressFamilyToQueryType(AddressFamily addressFamily) => { AddressFamily.InterNetwork => Interop.Dnsapi.DNS_TYPE_A, AddressFamily.InterNetworkV6 => Interop.Dnsapi.DNS_TYPE_AAAA, - _ => throw new ArgumentException(SR.net_invalid_ip_addr, nameof(addressFamily)), + _ => throw new ArgumentException(SR.net_dns_unsupported_address_family, nameof(addressFamily)), }; // ---- Query wrappers (issue the query, then parse the record list) ---- @@ -255,10 +255,26 @@ private static DnsResult MergeAddressResults(DnsResult TimeSpan.Zero ? a.NegativeCacheTtl : b.NegativeCacheTtl; + TimeSpan negTtl = MinNonZero(a.NegativeCacheTtl, b.NegativeCacheTtl); return new DnsResult(chosenRc, null, negTtl); } + // Returns the smaller of two non-zero negative-cache TTLs, or zero if neither is positive. + private static TimeSpan MinNonZero(TimeSpan x, TimeSpan y) + { + if (x <= TimeSpan.Zero) + { + return y > TimeSpan.Zero ? y : TimeSpan.Zero; + } + + if (y <= TimeSpan.Zero) + { + return x; + } + + return x < y ? x : y; + } + private static bool TryParseAddress(ushort recordType, IntPtr dataPtr, out IPAddress? address) { if (recordType == Interop.Dnsapi.DNS_TYPE_A) diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/LoopbackDnsServer.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/LoopbackDnsServer.cs index 004a0600759496..1cebcc837ed5f5 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/LoopbackDnsServer.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/LoopbackDnsServer.cs @@ -119,13 +119,19 @@ private async Task ListenUdpAsync(CancellationToken ct) EndPoint remote = result.RemoteEndPoint; _ = Task.Run(async () => { - Interlocked.Increment(ref _requestCount); - - byte[] response = ProcessQuery(query); - if (response.Length > 0) + try { - await _udp.SendToAsync(response, SocketFlags.None, remote, ct); + Interlocked.Increment(ref _requestCount); + + byte[] response = ProcessQuery(query); + if (response.Length > 0) + { + await _udp.SendToAsync(response, SocketFlags.None, remote, ct); + } } + catch (OperationCanceledException) { } + catch (ObjectDisposedException) { } + catch (SocketException) { } }); } }