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..e5d47b96426756 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsApi.cs @@ -0,0 +1,68 @@ +// 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 + { + // ---- 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; + + [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..f6203fe871fe94 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Dnsapi/Interop.DnsTypes.cs @@ -0,0 +1,199 @@ +// 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.CompilerServices; +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 char* QueryName; // PCWSTR + public ushort QueryType; + public ulong QueryOptions; + public DNS_ADDR_ARRAY* pDnsServerList; + public uint InterfaceIndex; + public delegate* unmanaged[Stdcall] 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 char* QueryName; + public ushort QueryType; + public ulong QueryOptions; + public DNS_ADDR_ARRAY* pDnsServerList; + public uint InterfaceIndex; + public delegate* unmanaged[Stdcall] pQueryCompletionCallback; + public IntPtr pQueryContext; + public int IsNetworkQueryRequired; // BOOL + public uint RequiredNetworkIndex; + 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 char* 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 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 + // sizeof(DNS_RECORD_HEADER) rather than a hard-coded offset. + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct DNS_RECORD_HEADER + { + public IntPtr pNext; // DNS_RECORD* + public char* 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 struct DNS_AAAA_DATA + { + public InlineArray16 Ip6Address; + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct DNS_PTR_DATA + { + public char* 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 unsafe struct DNS_CNAME_DATA + { + public char* pNameHost; + } + + internal unsafe struct DNS_NS_DATA + { + public char* pNameHost; + } +#pragma warning restore CS0649 + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct DNS_MX_DATA + { + public char* pNameExchange; // PCWSTR + public ushort wPreference; + public ushort Pad; + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct DNS_SRV_DATA + { + public char* 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 unsafe struct DNS_SOA_DATA + { + public char* pNamePrimaryServer; // PCWSTR + public char* 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..2b826902414435 100644 --- a/src/libraries/System.Net.NameResolution/ref/System.Net.NameResolution.cs +++ b/src/libraries/System.Net.NameResolution/ref/System.Net.NameResolution.cs @@ -6,6 +6,20 @@ namespace System.Net { + 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 CNameRecord + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public string CanonicalName { get { throw null; } } + public System.TimeSpan Ttl { get { throw null; } } + } public static partial class Dns { public static System.IAsyncResult BeginGetHostAddresses(string hostNameOrAddress, System.AsyncCallback? requestCallback, object? state) { throw null; } @@ -42,6 +56,73 @@ 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.Net.Sockets.AddressFamily addressFamily, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task> ResolveAddressesAsync(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 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 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 static System.Net.DnsResult ResolvePtr(System.Net.IPAddress address) { throw null; } + public static System.Net.DnsResult ResolvePtr(string name) { 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.Threading.Tasks.Task> ResolvePtrAsync(string name, 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 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 sealed partial class DnsResolver : System.IAsyncDisposable, System.IDisposable + { + public DnsResolver() { } + public DnsResolver(System.Net.DnsResolverOptions options) { } + public void Dispose() { } + public System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } + 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.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> ResolveAddressesAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public System.Net.DnsResult ResolveCName(string name) { throw null; } + public System.Threading.Tasks.Task> ResolveCNameAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public System.Net.DnsResult ResolveMx(string name) { throw null; } + public System.Threading.Tasks.Task> ResolveMxAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public System.Net.DnsResult ResolveNs(string name) { throw null; } + public System.Threading.Tasks.Task> ResolveNsAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public System.Net.DnsResult ResolvePtr(System.Net.IPAddress address) { throw null; } + public System.Net.DnsResult ResolvePtr(string name) { 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> ResolvePtrAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public System.Net.DnsResult ResolveSrv(string name) { throw null; } + public System.Threading.Tasks.Task> ResolveSrvAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public System.Net.DnsResult ResolveTxt(string name) { throw null; } + public System.Threading.Tasks.Task> ResolveTxtAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + } + public sealed partial class DnsResolverOptions + { + public DnsResolverOptions() { } + public System.Collections.Generic.IList Servers { get { throw null; } set { } } + } + [System.CLSCompliantAttribute(false)] + public enum DnsResponseCode : ushort + { + NoError = (ushort)0, + FormatError = (ushort)1, + ServerFailure = (ushort)2, + NxDomain = (ushort)3, + NotImplemented = (ushort)4, + Refused = (ushort)5, + } + public readonly partial struct DnsResult + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public System.TimeSpan NegativeCacheTtl { get { throw null; } } + public System.Collections.Generic.IReadOnlyList Records { get { throw null; } } + [System.CLSCompliantAttribute(false)] + public System.Net.DnsResponseCode ResponseCode { get { throw null; } } } public partial class IPHostEntry { @@ -50,4 +131,48 @@ public IPHostEntry() { } public string[] Aliases { get { throw null; } set { } } public string HostName { get { throw null; } set { } } } + 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 NsRecord + { + 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 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 SrvRecord + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public System.Collections.Generic.IReadOnlyList Addresses { get { throw null; } } + [System.CLSCompliantAttribute(false)] + public ushort Port { get { throw null; } } + [System.CLSCompliantAttribute(false)] + public ushort Priority { get { throw null; } } + public string Target { get { throw null; } } + public System.TimeSpan Ttl { get { throw null; } } + [System.CLSCompliantAttribute(false)] + public ushort Weight { get { throw null; } } + } + public readonly partial struct TxtRecord + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public System.TimeSpan Ttl { get { throw null; } } + public System.Collections.Generic.IReadOnlyList Values { 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..5333f8ea48b72c 100644 --- a/src/libraries/System.Net.NameResolution/src/Resources/Strings.resx +++ b/src/libraries/System.Net.NameResolution/src/Resources/Strings.resx @@ -75,4 +75,16 @@ System.Net.NameResolution is not supported on this platform. + + 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. + + + 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.NameResolution.csproj b/src/libraries/System.Net.NameResolution/src/System.Net.NameResolution.csproj index 47439bf44a3f63..4cdb503467afc2 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,12 @@ + + + + @@ -76,6 +88,7 @@ + + field ??= new(); + + /// + /// 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) + => DefaultResolver.ResolvePtr(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); + + /// + /// 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/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..7a10fc3000c420 --- /dev/null +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsRecords.cs @@ -0,0 +1,135 @@ +// 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 +{ + /// 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) + { + Address = address; + Ttl = ttl; + } + } + + /// 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) + { + Target = target; + Port = port; + Priority = priority; + Weight = weight; + Ttl = ttl; + _addresses = addresses; + } + } + + /// 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) + { + Exchange = exchange; + Preference = preference; + Ttl = ttl; + } + } + + /// 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) + { + _values = values; + Ttl = ttl; + } + } + + /// 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) + { + CanonicalName = canonicalName; + Ttl = ttl; + } + } + + /// 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) + { + Name = name; + Ttl = ttl; + } + } + + /// 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) + { + Name = name; + Ttl = 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 new file mode 100644 index 00000000000000..dddf75b3d377c5 --- /dev/null +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs @@ -0,0 +1,530 @@ +// 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; +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 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); + // 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]; + 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); + } + } + + /// + /// 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); + ObjectDisposedException.ThrowIf(_disposed, this); + Task> task = ResolveAddressesCore(async: false, name, addressFamily, default); + Debug.Assert(task.IsCompleted); + 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); + ObjectDisposedException.ThrowIf(_disposed, this); + Task> task = ResolveSrvCore(async: false, name, default); + Debug.Assert(task.IsCompleted); + 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); + ObjectDisposedException.ThrowIf(_disposed, this); + Task> task = ResolveMxCore(async: false, name, default); + Debug.Assert(task.IsCompleted); + 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); + ObjectDisposedException.ThrowIf(_disposed, this); + Task> task = ResolveTxtCore(async: false, name, default); + Debug.Assert(task.IsCompleted); + 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); + ObjectDisposedException.ThrowIf(_disposed, this); + Task> task = ResolveCNameCore(async: false, name, default); + Debug.Assert(task.IsCompleted); + 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); + ObjectDisposedException.ThrowIf(_disposed, this); + Task> task = ResolvePtrCore(async: false, name, default); + Debug.Assert(task.IsCompleted); + 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. + /// + /// 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); + ObjectDisposedException.ThrowIf(_disposed, this); + Task> task = ResolveNsCore(async: false, name, default); + Debug.Assert(task.IsCompleted); + 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); + ObjectDisposedException.ThrowIf(_disposed, this); + 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); + ObjectDisposedException.ThrowIf(_disposed, this); + 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); + ObjectDisposedException.ThrowIf(_disposed, this); + 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); + ObjectDisposedException.ThrowIf(_disposed, this); + 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); + ObjectDisposedException.ThrowIf(_disposed, this); + 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); + ObjectDisposedException.ThrowIf(_disposed, this); + 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. + /// + /// 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); + ObjectDisposedException.ThrowIf(_disposed, this); + return ResolveNsCore(async: true, name, cancellationToken); + } + + /// + public void Dispose() => _disposed = true; + + /// + public ValueTask DisposeAsync() + { + _disposed = true; + 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 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. 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 async Task> ResolveAddressesCore(bool async, string name, AddressFamily addressFamily, CancellationToken cancellationToken) + { + if (addressFamily == AddressFamily.Unspecified) + { + // if `async == true` then this runs both queries in parallel, otherwise it runs them sequentially (the synchronous path is expected to be rare and the async path is expected to be the common case). + Task> aTask = DoResolve(async, name, AddressFamily.InterNetwork, cancellationToken); + Task> aaaaTask = DoResolve(async, name, AddressFamily.InterNetworkV6, cancellationToken); + + await Task.WhenAll(aTask, aaaaTask).ConfigureAwait(false); + DnsResult aRes = await aTask.ConfigureAwait(false); + DnsResult aaaaRes = await aaaaTask.ConfigureAwait(false); + return MergeAddressResults(aRes, aaaaRes); + } + + return await DoResolve(async, name, addressFamily, cancellationToken).ConfigureAwait(false); + + Task> DoResolve(bool async, string name, AddressFamily addressFamily, CancellationToken cancellationToken) + => NameResolutionTelemetry.AnyDiagnosticsEnabled() + ? 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, (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, (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, (servers: _servers, async, name, cancellationToken), + static s => DnsResolverPal.ResolveTxt(s.servers, s.async, s.name, s.cancellationToken), + static r => + { + 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, (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, (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, (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, TState state, Func>> resolve, Func, string[]> getAnswers) + { + NameResolutionActivity activity = NameResolutionTelemetry.Log.BeforeResolution(name); + try + { + DnsResult result = await resolve(state).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; + } + + // Combines the results of the separate A and AAAA queries issued for an + // AddressFamily.Unspecified request into a single result. + private static DnsResult MergeAddressResults(DnsResult a, DnsResult b) + { + if (a.Records.Count > 0 || b.Records.Count > 0) + { + AddressRecord[] merged = [.. a.Records, .. b.Records]; + TimeSpan mergedTtl = MinNonZero(a.NegativeCacheTtl, b.NegativeCacheTtl); + return new DnsResult(DnsResponseCode.NoError, merged, mergedTtl); + } + + DnsResponseCode chosenRc = a.ResponseCode == DnsResponseCode.NxDomain || b.ResponseCode == DnsResponseCode.NxDomain + ? DnsResponseCode.NxDomain + : (a.ResponseCode != DnsResponseCode.NoError ? a.ResponseCode : b.ResponseCode); + 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 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 $"{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[16 * 4]; + int pos = 0; + for (int i = bytes.Length - 1; i >= 0; i--) + { + byte b = bytes[i]; + chars[pos++] = ToHex(b & 0xF); + chars[pos++] = '.'; + chars[pos++] = ToHex(b >> 4); + chars[pos++] = '.'; + } + return string.Concat(chars, "ip6.arpa"); + + static char ToHex(int n) => (char)(n < 10 ? '0' + n : 'a' + (n - 10)); + } + else + { + throw new ArgumentException(SR.net_dns_unsupported_address_family, 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..2f20f8064a2e41 --- /dev/null +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverOptions.cs @@ -0,0 +1,29 @@ +// 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 + { + private IList _servers = new List(); + + /// + /// Gets or sets the DNS servers to query. When empty, the system-configured DNS servers are used. + /// + /// 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.Unsupported.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Unsupported.cs new file mode 100644 index 00000000000000..4849146c1dc952 --- /dev/null +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Unsupported.cs @@ -0,0 +1,33 @@ +// 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 +{ + internal static partial class DnsResolverPal + { + public static Task> ResolveAddresses(IPEndPoint[] servers, bool async, string name, AddressFamily addressFamily, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + + public static Task> ResolveSrv(IPEndPoint[] servers, bool async, string name, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + + public static Task> ResolveMx(IPEndPoint[] servers, bool async, string name, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + + public static Task> ResolveTxt(IPEndPoint[] servers, bool async, string name, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + + public static Task> ResolveCName(IPEndPoint[] servers, bool async, string name, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + + public static Task> ResolvePtr(IPEndPoint[] servers, bool async, string name, CancellationToken cancellationToken) + => throw new PlatformNotSupportedException(); + + public static Task> ResolveNs(IPEndPoint[] 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..4f93b021d6d277 --- /dev/null +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.Windows.cs @@ -0,0 +1,759 @@ +// 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.CompilerServices; +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(IPEndPoint[] servers, bool async, string name, AddressFamily addressFamily, CancellationToken cancellationToken) + { + ushort qtype = AddressFamilyToQueryType(addressFamily); + return await QueryAddresses(servers, async, name, qtype, cancellationToken).ConfigureAwait(false); + } + + public static async Task> ResolveSrv(IPEndPoint[] servers, bool async, string name, CancellationToken cancellationToken) + { + using DnsQueryRawResult raw = await DnsQueryEx(servers, async, name, Interop.Dnsapi.DNS_TYPE_SRV, cancellationToken).ConfigureAwait(false); + return ParseSrv(raw); + } + + public static Task> ResolveMx(IPEndPoint[] servers, bool async, string name, CancellationToken cancellationToken) + => QuerySimple(servers, async, name, Interop.Dnsapi.DNS_TYPE_MX, cancellationToken, s_parseMx); + + public static Task> ResolveCName(IPEndPoint[] servers, bool async, string name, CancellationToken cancellationToken) + => QuerySimple(servers, async, name, Interop.Dnsapi.DNS_TYPE_CNAME, cancellationToken, s_parseCName); + + public static Task> ResolvePtr(IPEndPoint[] servers, bool async, string name, CancellationToken cancellationToken) + => QuerySimple(servers, async, name, Interop.Dnsapi.DNS_TYPE_PTR, cancellationToken, s_parsePtr); + + public static Task> ResolveNs(IPEndPoint[] servers, bool async, string name, CancellationToken cancellationToken) + => QuerySimple(servers, async, name, Interop.Dnsapi.DNS_TYPE_NS, cancellationToken, s_parseNs); + + public static async Task> ResolveTxt(IPEndPoint[] servers, bool async, string name, CancellationToken cancellationToken) + { + 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) ---- + // The bodies dereference PCWSTR fields, so the methods are unsafe; caching the + // delegates avoids re-allocating one per query. + + private static readonly Func s_parseMx = ParseMxRecord; + private static readonly Func s_parseCName = ParseCNameRecord; + private static readonly Func s_parsePtr = ParsePtrRecord; + private static readonly Func s_parseNs = ParseNsRecord; + + private static unsafe MxRecord ParseMxRecord(Interop.Dnsapi.DNS_RECORD_HEADER hdr, IntPtr dataPtr) + { + ref readonly Interop.Dnsapi.DNS_MX_DATA data = ref AsStruct(dataPtr); + return new MxRecord(PtrToString(data.pNameExchange) ?? string.Empty, data.wPreference, TimeSpan.FromSeconds(hdr.dwTtl)); + } + + private static unsafe CNameRecord ParseCNameRecord(Interop.Dnsapi.DNS_RECORD_HEADER hdr, IntPtr dataPtr) + { + ref readonly Interop.Dnsapi.DNS_CNAME_DATA data = ref AsStruct(dataPtr); + return new CNameRecord(PtrToString(data.pNameHost) ?? string.Empty, TimeSpan.FromSeconds(hdr.dwTtl)); + } + + private static unsafe PtrRecord ParsePtrRecord(Interop.Dnsapi.DNS_RECORD_HEADER hdr, IntPtr dataPtr) + { + ref readonly Interop.Dnsapi.DNS_PTR_DATA data = ref AsStruct(dataPtr); + return new PtrRecord(PtrToString(data.pNameHost) ?? string.Empty, TimeSpan.FromSeconds(hdr.dwTtl)); + } + + private static unsafe NsRecord ParseNsRecord(Interop.Dnsapi.DNS_RECORD_HEADER hdr, IntPtr dataPtr) + { + ref readonly Interop.Dnsapi.DNS_NS_DATA data = ref AsStruct(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_dns_unsupported_address_family, nameof(addressFamily)), + }; + + // ---- Query wrappers (issue the query, then parse the record list) ---- + + private static async Task> QueryAddresses(IPEndPoint[] servers, bool async, string name, ushort qtype, CancellationToken cancellationToken) + { + using DnsQueryRawResult raw = await DnsQueryEx(servers, async, name, qtype, cancellationToken).ConfigureAwait(false); + return ParseAddresses(raw, qtype); + } + + private static async Task> QuerySimple(IPEndPoint[] servers, bool async, string name, ushort qtype, CancellationToken cancellationToken, + Func selector) + { + using DnsQueryRawResult raw = await DnsQueryEx(servers, async, name, qtype, cancellationToken).ConfigureAwait(false); + return ParseSimple(raw, qtype, selector); + } + + // ---- Record-list parsers ---- + + private static unsafe 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;) + { + ref readonly Interop.Dnsapi.DNS_RECORD_HEADER hdr = ref AsStruct(cur); + uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; + if (hdr.wType == qtype && section == Interop.Dnsapi.DNSREC_ANSWER) + { + IntPtr dataPtr = cur + sizeof(Interop.Dnsapi.DNS_RECORD_HEADER); + 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, raw.NegativeCacheTtl); + } + + private static unsafe 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;) + { + ref readonly Interop.Dnsapi.DNS_RECORD_HEADER hdr = ref AsStruct(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 + sizeof(Interop.Dnsapi.DNS_RECORD_HEADER); + ref readonly Interop.Dnsapi.DNS_SRV_DATA data = ref AsStruct(dataPtr); + string target = PtrToString(data.pNameTarget) ?? string.Empty; + 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, raw.NegativeCacheTtl); + } + + private static unsafe 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;) + { + ref readonly Interop.Dnsapi.DNS_RECORD_HEADER hdr = ref AsStruct(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 + sizeof(Interop.Dnsapi.DNS_RECORD_HEADER); + ref readonly Interop.Dnsapi.DNS_TXT_DATA data = ref AsStruct(dataPtr); + // DNS_TXT_DATA: uint dwStringCount; followed by array of PCWSTR. + uint count = data.dwStringCount; + IntPtr stringsPtr = dataPtr + sizeof(Interop.Dnsapi.DNS_TXT_DATA); + if (IntPtr.Size > sizeof(Interop.Dnsapi.DNS_TXT_DATA)) + { + // Round up to pointer alignment. + long aligned = ((long)stringsPtr + (IntPtr.Size - 1)) & ~(long)(IntPtr.Size - 1); + stringsPtr = checked((nint)aligned); + } + string[] values = new string[count]; + for (int i = 0; i < count; i++) + { + IntPtr strPtr = Marshal.ReadIntPtr(stringsPtr, i * IntPtr.Size); + values[i] = PtrToString(strPtr) ?? string.Empty; + } + records.Add(new TxtRecord(values, TimeSpan.FromSeconds(hdr.dwTtl))); + } + cur = hdr.pNext; + } + + return new DnsResult(DnsResponseCode.NoError, records, raw.NegativeCacheTtl); + } + + private static unsafe 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;) + { + ref readonly Interop.Dnsapi.DNS_RECORD_HEADER hdr = ref AsStruct(cur); + uint section = hdr.Flags & Interop.Dnsapi.DNSREC_SECTION_MASK; + if (hdr.wType == qtype && section == Interop.Dnsapi.DNSREC_ANSWER) + { + IntPtr dataPtr = cur + sizeof(Interop.Dnsapi.DNS_RECORD_HEADER); + records.Add(selector(hdr, dataPtr)); + } + cur = hdr.pNext; + } + + return new DnsResult(DnsResponseCode.NoError, records, raw.NegativeCacheTtl); + } + + private static bool TryParseAddress(ushort recordType, IntPtr dataPtr, out IPAddress? address) + { + if (recordType == Interop.Dnsapi.DNS_TYPE_A) + { + // 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. + ref readonly Interop.Dnsapi.DNS_A_DATA data = ref AsStruct(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. + ref readonly Interop.Dnsapi.DNS_AAAA_DATA data = ref AsStruct(dataPtr); + address = new IPAddress((ReadOnlySpan)data.Ip6Address); + return true; + } + + address = null; + return false; + } + + private static unsafe void ParseAdditionalAddresses(IntPtr head, ref Dictionary>? glue) + { + for (IntPtr cur = head; cur != IntPtr.Zero;) + { + ref readonly Interop.Dnsapi.DNS_RECORD_HEADER hdr = ref AsStruct(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 + sizeof(Interop.Dnsapi.DNS_RECORD_HEADER); + if (TryParseAddress(hdr.wType, dataPtr, out IPAddress? address)) + { + string name = PtrToString(hdr.pName) ?? string.Empty; + glue ??= new Dictionary>(StringComparer.OrdinalIgnoreCase); + List list = CollectionsMarshal.GetValueRefOrAddDefault(glue, name, out _) ??= new List(); + list.Add(new AddressRecord(address!, TimeSpan.FromSeconds(hdr.dwTtl))); + } + } + cur = hdr.pNext; + } + } + + // ---- Core DnsQueryEx wrapper ---- + + private static Task DnsQueryEx(IPEndPoint[] 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 string? PtrToString(IntPtr p) => + p == IntPtr.Zero ? null : Marshal.PtrToStringUni(p); + + private static unsafe string? PtrToString(char* p) => + p == null ? null : new string(p); + + // Reinterprets an unmanaged pointer as a readonly reference to a struct, avoiding the + // marshalling copy of Marshal.PtrToStructure. The single pointer cast is confined here. + private static unsafe ref readonly T AsStruct(IntPtr ptr) where T : unmanaged => + ref Unsafe.AsRef((void*)ptr); + + // ---- Asynchronous DnsQueryEx state machine ---- + + // Bundles the three fixed-size native structures DnsQueryEx needs into a single block so + // one SafeHandle can own their lifetime. The query name and server-list buffers are + // variable-length, so they are allocated separately and freed by the handle as well. + [StructLayout(LayoutKind.Sequential)] + private struct DnsQueryBuffers + { + public Interop.Dnsapi.DNS_QUERY_REQUEST Request; + public Interop.Dnsapi.DNS_QUERY_RESULT Result; + public Interop.Dnsapi.DNS_QUERY_CANCEL Cancel; + } + + // Owns the native buffers backing a single asynchronous DnsQueryEx call. SafeHandle + // reference counting is what makes cancellation thread-safe: DnsCancelQuery runs under + // DangerousAddRef so the Cancel buffer cannot be freed while a cancel call is in flight, + // and once the completion path disposes the handle any later cancellation attempt observes + // a closed handle and becomes a no-op instead of dereferencing freed memory. + private sealed unsafe class DnsQuerySafeHandle : SafeHandle + { + private IntPtr _namePtr; + private IntPtr _serverListPtr; + + public DnsQuerySafeHandle() + : base(IntPtr.Zero, ownsHandle: true) + { + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + public DnsQueryBuffers* Buffers => (DnsQueryBuffers*)handle; + + public void Allocate(string name, IPEndPoint[] servers, ushort queryType, + delegate* unmanaged[Stdcall] completionCallback, IntPtr context) + { + DnsQueryBuffers* buffers = (DnsQueryBuffers*)NativeMemory.AllocZeroed((nuint)sizeof(DnsQueryBuffers)); + SetHandle((IntPtr)buffers); + + _namePtr = Marshal.StringToHGlobalUni(name); + + buffers->Result.Version = Interop.Dnsapi.DNS_QUERY_REQUEST_VERSION1; + + buffers->Request.Version = Interop.Dnsapi.DNS_QUERY_REQUEST_VERSION1; + buffers->Request.QueryName = (char*)_namePtr; + buffers->Request.QueryType = queryType; + buffers->Request.QueryOptions = Interop.Dnsapi.DNS_QUERY_STANDARD; + buffers->Request.pQueryCompletionCallback = completionCallback; + buffers->Request.pQueryContext = context; + + if (servers is { Length: > 0 }) + { + BuildAddrArray(servers, out _serverListPtr); + buffers->Request.pDnsServerList = (Interop.Dnsapi.DNS_ADDR_ARRAY*)_serverListPtr; + } + } + + protected override bool ReleaseHandle() + { + if (_namePtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(_namePtr); + } + if (_serverListPtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(_serverListPtr); + } + NativeMemory.Free((void*)handle); + return true; + } + } + + /// + /// Holds the managed state for a single asynchronous DnsQueryEx invocation: the native + /// buffers (via ), the GC handle handed to the native + /// completion callback, the cancellation registration, and the completion source. + /// + private sealed unsafe class DnsQueryAsyncState + { + private readonly TaskCompletionSource _tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly string _name; + private readonly ushort _queryType; + private readonly CancellationToken _cancellationToken; + private readonly IPEndPoint[] _servers; + private readonly object _lock = new(); + + private DnsQuerySafeHandle _handle = null!; + private GCHandle _selfHandle; + private CancellationTokenRegistration _ctReg; + private bool _completed; + + public DnsQueryAsyncState(IPEndPoint[] servers, string name, ushort queryType, CancellationToken cancellationToken) + { + _servers = servers; + _name = name; + _queryType = queryType; + _cancellationToken = cancellationToken; + } + + public Task StartAsync() + { + ValidateServerPorts(_servers); + + _handle = new DnsQuerySafeHandle(); + _selfHandle = new GCHandle(this); + + int status; + try + { + _handle.Allocate(_name, _servers, _queryType, &QueryCompletionCallback, + GCHandle.ToIntPtr(_selfHandle)); + + DnsQueryBuffers* buffers = _handle.Buffers; + status = Interop.Dnsapi.DnsQueryEx(&buffers->Request, &buffers->Result, &buffers->Cancel); + } + catch + { + _handle.Dispose(); + _selfHandle.Dispose(); + throw; + } + + if (status == Interop.Dnsapi.DNS_REQUEST_PENDING) + { + // The query is in-flight; the native runtime owns the buffers until the + // completion callback fires. Register cancellation, then publish the + // registration under _lock and check whether completion already ran (it may + // race ahead on another thread the instant DnsQueryEx returned PENDING). If it + // did, we dispose the registration here; otherwise CompleteFromResult disposes + // it. Either way the SafeHandle keeps the Cancel buffer alive across any + // DnsCancelQuery call, so cancellation can never touch freed memory. + CancellationTokenRegistration registration = _cancellationToken.UnsafeRegister(static (s, _) => + { + ((DnsQueryAsyncState)s!).CancelAndAbort(); + }, this); + + bool alreadyCompleted; + lock (_lock) + { + _ctReg = registration; + alreadyCompleted = _completed; + } + + if (alreadyCompleted) + { + registration.Dispose(); + } + } + else + { + // Synchronous completion: the callback was NOT invoked; we complete inline. + CompleteFromResult(status); + } + + return _tcs.Task; + } + + private void CancelAndAbort() + { + bool addedRef = false; + try + { + // AddRef pins the native buffers for the duration of the cancel call. If the + // completion path has already disposed the handle, AddRef throws and we treat + // the cancellation as a no-op. + _handle.DangerousAddRef(ref addedRef); + Interop.Dnsapi.DnsCancelQuery(&_handle.Buffers->Cancel); + } + catch (ObjectDisposedException) + { + // Completion already disposed the handle and freed the buffers; nothing left + // to cancel. + } + finally + { + if (addedRef) + { + _handle.DangerousRelease(); + } + } + } + + /// + /// Invoked exactly once, from either the native completion callback or the inline + /// synchronous-completion path. Parses the QueryStatus and pQueryRecords from the + /// result struct, completes the TCS, and releases all native state. + /// + internal void CompleteFromResult(int status) + { + CancellationTokenRegistration registration; + lock (_lock) + { + _completed = true; + registration = _ctReg; + } + + // Dispose the registration outside the lock. This also waits for any in-flight + // CancelAndAbort to finish before we dispose the handle below, so DnsCancelQuery + // can never observe a freed buffer. + registration.Dispose(); + + try + { + IntPtr records = _handle.Buffers->Result.pQueryRecords; + + if (_cancellationToken.IsCancellationRequested) + { + if (records != IntPtr.Zero) + { + Interop.Dnsapi.DnsFree(records, Interop.Dnsapi.DnsFreeRecordList); + } + _tcs.TrySetCanceled(_cancellationToken); + return; + } + + DnsResponseCode rc = MapWindowsErrorToResponseCode(status); + + // 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)); + } + catch (Exception ex) + { + _tcs.TrySetException(ex); + } + finally + { + _handle.Dispose(); + _selfHandle.Dispose(); + } + } + } + + // Native completion callback, invoked by DnsQueryEx on a thread pool thread. + // Marked [UnmanagedCallersOnly] so it can be passed directly as a function + // pointer without an intermediate marshalled delegate. +#pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })] +#pragma warning restore CS3016 + private static unsafe void QueryCompletionCallback(nint pQueryContext, nint pQueryResults) + { + try + { + DnsQueryAsyncState state = GCHandle.FromIntPtr(pQueryContext).Target; + + // pQueryResults points to the same DNS_QUERY_RESULT we passed in. + Interop.Dnsapi.DNS_QUERY_RESULT* res = (Interop.Dnsapi.DNS_QUERY_RESULT*)pQueryResults; + state.CompleteFromResult(res->QueryStatus); + } + catch (Exception ex) + { + // Never allow exceptions to propagate into native code. + Debug.Fail($"Unexpected exception in DnsQueryEx completion callback: {ex}"); + } + } + + // 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(IPEndPoint[] servers) + { + 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(IPEndPoint[] 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 = (char*)namePtr; + request.QueryType = queryType; + request.QueryOptions = Interop.Dnsapi.DNS_QUERY_STANDARD; + // No completion callback => synchronous execution. + + if (servers is { Length: > 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); + + // 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); + } + finally + { + if (namePtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(namePtr); + } + if (serverListPtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(serverListPtr); + } + } + } + + private static unsafe void BuildAddrArray(IPEndPoint[] servers, out IntPtr arrayPtr) + { + int count = servers.Length; + + // 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(DnsResolverOptions.Servers)); + } + } + + int headerSize = sizeof(Interop.Dnsapi.DNS_ADDR_ARRAY); + int addrSize = sizeof(Interop.Dnsapi.DNS_ADDR); + int totalSize = checked(headerSize + addrSize * count); + + arrayPtr = Marshal.AllocHGlobal(totalSize); + + // 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); + + for (int i = 0; i < count; 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, IPAddress address) + { + // 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); + } + + 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 unsafe TimeSpan ExtractNegativeCacheTtl(IntPtr head) + { + // Walk the record list looking for an SOA in the authority section. + for (IntPtr cur = head; cur != IntPtr.Zero;) + { + ref readonly Interop.Dnsapi.DNS_RECORD_HEADER hdr = ref AsStruct(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 + sizeof(Interop.Dnsapi.DNS_RECORD_HEADER); + ref readonly Interop.Dnsapi.DNS_SOA_DATA soa = ref AsStruct(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); + } + } + } + } +} 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..42f1b940c0fa0b --- /dev/null +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResult.cs @@ -0,0 +1,42 @@ +// 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 +{ + /// + /// 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; + + /// Gets the DNS response code returned by the server. + [CLSCompliant(false)] + public DnsResponseCode ResponseCode { get; } + + /// + /// Gets the records returned by the server. The list is empty on error or NODATA responses. + /// + public IReadOnlyList Records => _records ?? Array.Empty(); + + /// + /// 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) + { + ResponseCode = responseCode; + _records = records; + NegativeCacheTtl = negativeCacheTtl; + } + } +} 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 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..6651a1984afe18 --- /dev/null +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs @@ -0,0 +1,525 @@ +// 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; +using Xunit; + +namespace System.Net.NameResolution.Tests +{ + public sealed class WindowsLoopbackServer : IAsyncDisposable + { + private LoopbackDnsServer? _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 is not null) + { + await _server.DisposeAsync(); + } + } + } + + // 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 + // 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. + // + // 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.")] + [Collection(nameof(DisableParallelization))] + public class DnsResolverLoopbackTest : IClassFixture + { + 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"; + + LoopbackDnsServer _server; + DnsResolver? _resolver; + + public DnsResolverLoopbackTest(WindowsLoopbackServer fixture) + { + _server = fixture.Server; + _server.ClearResponses(); + } + + 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 ---- + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [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 ResolveAddresses(async, Resolver, 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"); + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [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 ResolveAddresses(async, Resolver, 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()); + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [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 ResolveAddresses(async, Resolver, name); + + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + AddressRecord record = Assert.Single(result.Records); + Assert.Equal("fd00::1", record.Address.ToString()); + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [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 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()); + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [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 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). + Assert.True(record.Ttl > TimeSpan.Zero && record.Ttl <= TimeSpan.FromSeconds(120), + $"Unexpected TTL: {record.Ttl}"); + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveAddresses_Nxdomain_ReturnsNxDomain(bool async) + { + 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 ResolveAddresses(async, Resolver, name); + + 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}"); + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveAddresses_NoData_ReturnsNoErrorWithEmptyRecords(bool async) + { + 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 ResolveAddresses(async, Resolver, name); + + 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}"); + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveAddresses_NoData_And_Nxdomain_AreDistinguishable(bool async) + { + 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 ResolveAddresses(async, Resolver, nodataName); + Assert.Equal(DnsResponseCode.NoError, nodata.ResponseCode); + Assert.Empty(nodata.Records); + + DnsResult nxdomain = await ResolveAddresses(async, Resolver, missingName); + Assert.Equal(DnsResponseCode.NxDomain, nxdomain.ResponseCode); + Assert.Empty(nxdomain.Records); + + Assert.NotEqual(nodata.ResponseCode, nxdomain.ResponseCode); + } + + // ---- SRV ---- + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [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 ResolveSrv(async, Resolver, 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); + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveSrv_IncludesAdditionalAddresses(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) + .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 ResolveSrv(async, Resolver, 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); + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [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 ResolveSrv(async, Resolver, name); + + SrvRecord record = Assert.Single(result.Records); + Assert.Equal("noaddr.test", record.Target); + Assert.Empty(record.Addresses); + } + + // ---- MX / TXT / CNAME / PTR / NS ---- + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [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 ResolveMx(async, Resolver, 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); + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [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 ResolveTxt(async, Resolver, 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"); + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [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 ResolveCName(async, Resolver, name); + + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + CNameRecord record = Assert.Single(result.Records); + Assert.Equal("canonical.test", record.CanonicalName); + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [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 ResolvePtr(async, Resolver, name); + + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + PtrRecord record = Assert.Single(result.Records); + Assert.Equal("host.test", record.Name); + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [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 ResolveNs(async, Resolver, 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 ---- + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [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 + { + 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 ResolveAddresses(async, resolver, 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(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + public async Task ResolveAddresses_CancellationInFlight_Throws() + { + using SemaphoreSlim queryReceived = new(0, 1); + using ManualResetEventSlim serverCanContinue = new(false); + using CancellationTokenSource cts = new(); + + 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(); + } + + // ---- Telemetry ---- + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [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."); + } + } +} 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..706171b078aa68 --- /dev/null +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs @@ -0,0 +1,346 @@ +// 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 System.Linq; +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 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] + 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)); + Assert.Throws(() => r.ResolveAddresses(TestHost)); + Assert.Throws(() => r.ResolveSrv(TestSrv)); + Assert.Throws(() => r.ResolveMx(TestMxHost)); + } + + [Fact] + public async Task DnsResolver_DisposeAsync_ThrowsOnUse() + { + DnsResolver r = new DnsResolver(); + await r.DisposeAsync(); + 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))] + 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)); + } + + // 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(bool async) + { + using DnsResolver r = new DnsResolver(); + DnsResult result = await ResolveAddresses(async, r, 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); + } + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] + [OuterLoop] + public async Task ResolveAddresses_IPv4Only_ReturnsOnlyIPv4(bool async) + { + using DnsResolver r = new DnsResolver(); + DnsResult result = await ResolveAddresses(async, r, TestHost, AddressFamily.InterNetwork); + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + foreach (AddressRecord rec in result.Records) + { + Assert.Equal(AddressFamily.InterNetwork, rec.Address.AddressFamily); + } + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] + [OuterLoop] + public async Task ResolveAddresses_NonExistent_ReturnsNxDomain(bool async) + { + using DnsResolver r = new DnsResolver(); + DnsResult result = await ResolveAddresses(async, r, NonExistentHost); + Assert.Equal(DnsResponseCode.NxDomain, result.ResponseCode); + Assert.Empty(result.Records); + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] + [OuterLoop] + public async Task ResolveMx_KnownName_ReturnsRecords(bool async) + { + using DnsResolver r = new DnsResolver(); + DnsResult result = await ResolveMx(async, r, TestMxHost); + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + Assert.NotEmpty(result.Records); + foreach (MxRecord rec in result.Records) + { + Assert.False(string.IsNullOrEmpty(rec.Exchange)); + } + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] + [OuterLoop] + public async Task ResolveTxt_KnownName_ReturnsRecords(bool async) + { + using DnsResolver r = new DnsResolver(); + DnsResult result = await ResolveTxt(async, r, TestTxtHost); + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + Assert.NotEmpty(result.Records); + foreach (TxtRecord rec in result.Records) + { + Assert.NotEmpty(rec.Values); + } + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] + [OuterLoop] + public async Task ResolveCName_KnownName_ReturnsRecord(bool async) + { + using DnsResolver r = new DnsResolver(); + 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) + { + Assert.False(string.IsNullOrEmpty(result.Records[0].CanonicalName)); + } + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] + [OuterLoop] + public async Task ResolveNs_KnownName_ReturnsRecords(bool async) + { + using DnsResolver r = new DnsResolver(); + DnsResult result = await ResolveNs(async, r, TestNsHost); + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + Assert.NotEmpty(result.Records); + foreach (NsRecord rec in result.Records) + { + Assert.False(string.IsNullOrEmpty(rec.Name)); + } + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] + [OuterLoop] + public async Task ResolvePtr_ByIPAddress_ReturnsRecord(bool async) + { + using DnsResolver r = new DnsResolver(); + 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)); + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] + [OuterLoop] + public async Task Static_Dns_ResolveAddresses_Works(bool async) + { + DnsResult result = await Static_ResolveAddresses(async, TestHost); + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + Assert.NotEmpty(result.Records); + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] + [OuterLoop] + public async Task DnsResolver_CustomServer_Port53_Works(bool async) + { + 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) } + }; + using DnsResolver r = new DnsResolver(opts); + DnsResult result = await ResolveAddresses(async, r, TestHost); + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + 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)); + Assert.Throws(() => r.ResolveAddresses(TestHost)); + } + + // ---- Reverse-arpa name building (covers both IPv4 and IPv6 paths used by ResolvePtr(IPAddress)) ---- + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(false)] + [InlineData(true)] + [OuterLoop] + 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 ResolvePtr(async, r, 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/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..1cebcc837ed5f5 --- /dev/null +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/LoopbackDnsServer.cs @@ -0,0 +1,305 @@ +// 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.Diagnostics; +using System.Collections.Concurrent; +using System.Collections.Generic; +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 + { + public const int DnsPort = 53; + + private readonly Socket _udp; + private readonly Socket _tcp; + private readonly CancellationTokenSource _cts = new(); + private readonly Task _udpListenTask; + private readonly Task _tcpListenTask; + private readonly ConcurrentDictionary<(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(Socket udp, Socket tcp, IPEndPoint endPoint) + { + _udp = udp; + _tcp = tcp; + EndPoint = endPoint; + _udpListenTask = ListenUdpAsync(_cts.Token); + _tcpListenTask = ListenTcpAsync(_cts.Token); + } + + public static LoopbackDnsServer Start() + { + 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.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.LocalEndPoint!; + Socket tcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + try + { + 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})."); + } + + 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, _) => + 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) + { + 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 () => + { + try + { + 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) { } + }); + } + } + catch (OperationCanceledException) { } + catch (ObjectDisposedException) { } + catch (Exception ex) + { + Debug.Fail($"Unexpected exception in UDP listener: {ex}"); + } + } + + private async Task ListenTcpAsync(CancellationToken ct) + { + try + { + while (!ct.IsCancellationRequested) + { + 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(Socket client, CancellationToken ct) + { + try + { + using (client) + { + byte[] lengthBuf = new byte[2]; + if (!await ReadExactlyAsync(client, lengthBuf, ct)) + { + return; + } + + int queryLength = BinaryPrimitives.ReadUInt16BigEndian(lengthBuf); + byte[] query = new byte[queryLength]; + if (!await ReadExactlyAsync(client, 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 client.SendAsync(responseLengthBuf, SocketFlags.None, ct); + await client.SendAsync(response, SocketFlags.None, ct); + } + } + } + catch (OperationCanceledException) { } + catch (ObjectDisposedException) { } + catch (SocketException) { } + } + + private static async Task ReadExactlyAsync(Socket socket, byte[] buffer, CancellationToken ct) + { + int read = 0; + while (read < buffer.Length) + { + int n = await socket.ReceiveAsync(buffer.AsMemory(read), SocketFlags.None, 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.Dispose(); + 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 9741c993ccdba5..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 @@ -9,6 +9,10 @@ + + + +