diff --git a/src/installer/corehost/cli/apphost/static/configure.cmake b/src/installer/corehost/cli/apphost/static/configure.cmake index e3741d7e056a5..fe9e8a06a885f 100644 --- a/src/installer/corehost/cli/apphost/static/configure.cmake +++ b/src/installer/corehost/cli/apphost/static/configure.cmake @@ -1,4 +1,5 @@ include(CheckIncludeFiles) +include(CMakePushCheckState) check_include_files( GSS/GSS.h @@ -10,3 +11,16 @@ if (HeimdalGssApi) gssapi/gssapi.h HAVE_HEIMDAL_HEADERS) endif() + +if (CLR_CMAKE_TARGET_LINUX) + cmake_push_check_state(RESET) + set (CMAKE_REQUIRED_DEFINITIONS "-D_GNU_SOURCE") + set (CMAKE_REQUIRED_LIBRARIES "-lanl") + + check_symbol_exists( + getaddrinfo_a + netdb.h + HAVE_GETADDRINFO_A) + + cmake_pop_check_state() +endif () diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.HostEntry.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.HostEntry.cs index bfecbc412dbcd..12c2c3941d224 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.HostEntry.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.HostEntry.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; +using System.Net.Sockets; using System.Runtime.InteropServices; internal static partial class Interop @@ -32,8 +32,18 @@ internal unsafe struct HostEntry internal int IPAddressCount; // Number of IP addresses in the list } + [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_PlatformSupportsGetAddrInfoAsync")] + internal static extern bool PlatformSupportsGetAddrInfoAsync(); + [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetHostEntryForName")] - internal static extern unsafe int GetHostEntryForName(string address, System.Net.Sockets.AddressFamily family, HostEntry* entry); + internal static extern unsafe int GetHostEntryForName(string address, AddressFamily family, HostEntry* entry); + + [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetHostEntryForNameAsync")] + internal static extern unsafe int GetHostEntryForNameAsync( + string address, + AddressFamily family, + HostEntry* entry, + delegate* unmanaged callback); [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_FreeHostEntry")] internal static extern unsafe void FreeHostEntry(HostEntry* entry); diff --git a/src/libraries/Native/Unix/Common/pal_config.h.in b/src/libraries/Native/Unix/Common/pal_config.h.in index 0439c8922b3cc..cf23584a3e993 100644 --- a/src/libraries/Native/Unix/Common/pal_config.h.in +++ b/src/libraries/Native/Unix/Common/pal_config.h.in @@ -10,6 +10,7 @@ #cmakedefine01 HAVE_F_FULLFSYNC #cmakedefine01 HAVE_O_CLOEXEC #cmakedefine01 HAVE_GETIFADDRS +#cmakedefine01 HAVE_GETADDRINFO_A #cmakedefine01 HAVE_UTSNAME_DOMAINNAME #cmakedefine01 HAVE_STAT64 #cmakedefine01 HAVE_FORK diff --git a/src/libraries/Native/Unix/System.Native/entrypoints.c b/src/libraries/Native/Unix/System.Native/entrypoints.c index 4a6c65ac505e4..309eef0c8643f 100644 --- a/src/libraries/Native/Unix/System.Native/entrypoints.c +++ b/src/libraries/Native/Unix/System.Native/entrypoints.c @@ -115,7 +115,9 @@ static const Entry s_sysNative[] = DllImportEntry(SystemNative_ReadEvents) DllImportEntry(SystemNative_CreateNetworkChangeListenerSocket) DllImportEntry(SystemNative_CloseNetworkChangeListenerSocket) + DllImportEntry(SystemNative_PlatformSupportsGetAddrInfoAsync) DllImportEntry(SystemNative_GetHostEntryForName) + DllImportEntry(SystemNative_GetHostEntryForNameAsync) DllImportEntry(SystemNative_FreeHostEntry) DllImportEntry(SystemNative_GetNameInfo) DllImportEntry(SystemNative_GetDomainName) diff --git a/src/libraries/Native/Unix/System.Native/extra_libs.cmake b/src/libraries/Native/Unix/System.Native/extra_libs.cmake index 0b3f8424d2954..7a04e35f46e30 100644 --- a/src/libraries/Native/Unix/System.Native/extra_libs.cmake +++ b/src/libraries/Native/Unix/System.Native/extra_libs.cmake @@ -13,4 +13,8 @@ macro(append_extra_system_libs NativeLibsExtra) if (CLR_CMAKE_TARGET_IOS OR CLR_CMAKE_TARGET_TVOS) list(APPEND ${NativeLibsExtra} "-framework Foundation") endif () + + if (CLR_CMAKE_TARGET_LINUX AND HAVE_GETADDRINFO_A) + list(APPEND ${NativeLibsExtra} anl) + endif () endmacro() diff --git a/src/libraries/Native/Unix/System.Native/pal_networking.c b/src/libraries/Native/Unix/System.Native/pal_networking.c index 927c9cfe31af3..5e0f37635d326 100644 --- a/src/libraries/Native/Unix/System.Native/pal_networking.c +++ b/src/libraries/Native/Unix/System.Native/pal_networking.c @@ -60,6 +60,10 @@ #if HAVE_LINUX_CAN_H #include #endif +#if HAVE_GETADDRINFO_A +#include +#include +#endif #if HAVE_SYS_FILIO_H #include #endif @@ -335,37 +339,14 @@ static int32_t CopySockAddrToIPAddress(sockaddr* addr, sa_family_t family, IPAdd return -1; } -int32_t SystemNative_GetHostEntryForName(const uint8_t* address, int32_t addressFamily, HostEntry* entry) +static int32_t GetHostEntries(const uint8_t* address, struct addrinfo* info, HostEntry* entry) { - if (address == NULL || entry == NULL) - { - return GetAddrInfoErrorFlags_EAI_BADARG; - } - int32_t ret = GetAddrInfoErrorFlags_EAI_SUCCESS; - struct addrinfo* info = NULL; #if HAVE_GETIFADDRS struct ifaddrs* addrs = NULL; #endif - sa_family_t platformFamily; - if (!TryConvertAddressFamilyPalToPlatform(addressFamily, &platformFamily)) - { - return GetAddrInfoErrorFlags_EAI_FAMILY; - } - - struct addrinfo hint; - memset(&hint, 0, sizeof(struct addrinfo)); - hint.ai_flags = AI_CANONNAME; - hint.ai_family = platformFamily; - - int result = getaddrinfo((const char*)address, NULL, &hint, &info); - if (result != 0) - { - return ConvertGetAddrInfoAndGetNameInfoErrorsToPal(result); - } - entry->CanonicalName = NULL; entry->Aliases = NULL; entry->IPAddressList = NULL; @@ -393,7 +374,8 @@ int32_t SystemNative_GetHostEntryForName(const uint8_t* address, int32_t address #if HAVE_GETIFADDRS char name[_POSIX_HOST_NAME_MAX]; - result = gethostname((char*)name, _POSIX_HOST_NAME_MAX); + + int result = gethostname((char*)name, _POSIX_HOST_NAME_MAX); bool includeIPv4Loopback = true; bool includeIPv6Loopback = true; @@ -443,6 +425,8 @@ int32_t SystemNative_GetHostEntryForName(const uint8_t* address, int32_t address } } } +#else + (void)address; #endif if (entry->IPAddressCount > 0) @@ -519,6 +503,166 @@ int32_t SystemNative_GetHostEntryForName(const uint8_t* address, int32_t address return ret; } +#if HAVE_GETADDRINFO_A +struct GetAddrInfoAsyncState +{ + struct gaicb gai_request; + struct gaicb* gai_requests; + struct sigevent sigevent; + + struct addrinfo hint; + HostEntry* entry; + GetHostEntryForNameCallback callback; + char address[]; +}; + +static void GetHostEntryForNameAsyncComplete(sigval_t context) +{ + struct GetAddrInfoAsyncState* state = (struct GetAddrInfoAsyncState*)context.sival_ptr; + + atomic_thread_fence(memory_order_acquire); + + GetHostEntryForNameCallback callback = state->callback; + + int ret = ConvertGetAddrInfoAndGetNameInfoErrorsToPal(gai_error(&state->gai_request)); + + if (ret == 0) + { + const uint8_t* address = (const uint8_t*)state->address; + struct addrinfo* info = state->gai_request.ar_result; + + ret = GetHostEntries(address, info, state->entry); + } + + assert(callback != NULL); + callback(state->entry, ret); + + free(state); +} +#endif + +static bool TrySetAddressFamily(int32_t addressFamily, struct addrinfo* hint) +{ + sa_family_t platformFamily; + if (!TryConvertAddressFamilyPalToPlatform(addressFamily, &platformFamily)) + { + return false; + } + + memset(hint, 0, sizeof(struct addrinfo)); + + hint->ai_flags = AI_CANONNAME; + hint->ai_family = platformFamily; + + return true; +} + +int32_t SystemNative_PlatformSupportsGetAddrInfoAsync() +{ + return HAVE_GETADDRINFO_A; +} + +int32_t SystemNative_GetHostEntryForName(const uint8_t* address, int32_t addressFamily, HostEntry* entry) +{ + if (address == NULL || entry == NULL) + { + return GetAddrInfoErrorFlags_EAI_BADARG; + } + + struct addrinfo hint; + if (!TrySetAddressFamily(addressFamily, &hint)) + { + return GetAddrInfoErrorFlags_EAI_FAMILY; + } + + struct addrinfo* info = NULL; + + int result = getaddrinfo((const char*)address, NULL, &hint, &info); + if (result != 0) + { + return ConvertGetAddrInfoAndGetNameInfoErrorsToPal(result); + } + + return GetHostEntries(address, info, entry); +} + +int32_t SystemNative_GetHostEntryForNameAsync(const uint8_t* address, int32_t addressFamily, HostEntry* entry, GetHostEntryForNameCallback callback) +{ +#if HAVE_GETADDRINFO_A + if (address == NULL || entry == NULL) + { + return GetAddrInfoErrorFlags_EAI_BADARG; + } + + size_t addrlen = strlen((const char*)address); + + if (addrlen > _POSIX_HOST_NAME_MAX) + { + return GetAddrInfoErrorFlags_EAI_BADARG; + } + + sa_family_t platformFamily; + if (!TryConvertAddressFamilyPalToPlatform(addressFamily, &platformFamily)) + { + return GetAddrInfoErrorFlags_EAI_FAMILY; + } + + struct GetAddrInfoAsyncState* state = malloc(sizeof(*state) + addrlen + 1); + + if (state == NULL) + { + return GetAddrInfoErrorFlags_EAI_MEMORY; + } + + if (!TrySetAddressFamily(addressFamily, &state->hint)) + { + free(state); + return GetAddrInfoErrorFlags_EAI_FAMILY; + } + + memcpy(state->address, address, addrlen + 1); + + *state = (struct GetAddrInfoAsyncState) { + .gai_request = { + .ar_name = state->address, + .ar_service = NULL, + .ar_request = &state->hint, + .ar_result = NULL + }, + .gai_requests = &state->gai_request, + .sigevent = { + .sigev_notify = SIGEV_THREAD, + .sigev_value = { + .sival_ptr = state + }, + .sigev_notify_function = GetHostEntryForNameAsyncComplete + }, + .entry = entry, + .callback = callback + }; + + atomic_thread_fence(memory_order_release); + + int32_t result = getaddrinfo_a(GAI_NOWAIT, &state->gai_requests, 1, &state->sigevent); + + if (result != 0) + { + free(state); + return ConvertGetAddrInfoAndGetNameInfoErrorsToPal(result); + } + + return result; +#else + (void)address; + (void)addressFamily; + (void)entry; + (void)callback; + + // GetHostEntryForNameAsync is not supported on this platform. + return -1; +#endif +} + void SystemNative_FreeHostEntry(HostEntry* entry) { if (entry != NULL) @@ -555,13 +699,13 @@ static inline NativeFlagsType ConvertGetNameInfoFlagsToNative(int32_t flags) } int32_t SystemNative_GetNameInfo(const uint8_t* address, - int32_t addressLength, - int8_t isIPv6, - uint8_t* host, - int32_t hostLength, - uint8_t* service, - int32_t serviceLength, - int32_t flags) + int32_t addressLength, + int8_t isIPv6, + uint8_t* host, + int32_t hostLength, + uint8_t* service, + int32_t serviceLength, + int32_t flags) { assert(address != NULL); assert(addressLength > 0); diff --git a/src/libraries/Native/Unix/System.Native/pal_networking.h b/src/libraries/Native/Unix/System.Native/pal_networking.h index bbb0bc0785cce..69d0d1759bb5d 100644 --- a/src/libraries/Native/Unix/System.Native/pal_networking.h +++ b/src/libraries/Native/Unix/System.Native/pal_networking.h @@ -301,8 +301,16 @@ typedef struct uint32_t Padding; // Pad out to 8-byte alignment } SocketEvent; +PALEXPORT int32_t SystemNative_PlatformSupportsGetAddrInfoAsync(void); + PALEXPORT int32_t SystemNative_GetHostEntryForName(const uint8_t* address, int32_t addressFamily, HostEntry* entry); +typedef void (*GetHostEntryForNameCallback)(HostEntry* entry, int status); +PALEXPORT int32_t SystemNative_GetHostEntryForNameAsync(const uint8_t* address, + int32_t addressFamily, + HostEntry* entry, + GetHostEntryForNameCallback callback); + PALEXPORT void SystemNative_FreeHostEntry(HostEntry* entry); diff --git a/src/libraries/Native/Unix/configure.cmake b/src/libraries/Native/Unix/configure.cmake index 82e1ab4b5130b..8d4efdd1ad357 100644 --- a/src/libraries/Native/Unix/configure.cmake +++ b/src/libraries/Native/Unix/configure.cmake @@ -6,6 +6,7 @@ include(CheckPrototypeDefinition) include(CheckStructHasMember) include(CheckSymbolExists) include(CheckTypeSize) +include(CMakePushCheckState) # CMP0075 Include file check macros honor CMAKE_REQUIRED_LIBRARIES. if(POLICY CMP0075) @@ -900,6 +901,19 @@ check_symbol_exists( HAVE_INOTIFY_RM_WATCH) set (CMAKE_REQUIRED_LIBRARIES ${PREVIOUS_CMAKE_REQUIRED_LIBRARIES}) +if (CLR_CMAKE_TARGET_LINUX) + cmake_push_check_state(RESET) + set (CMAKE_REQUIRED_DEFINITIONS "-D_GNU_SOURCE") + set (CMAKE_REQUIRED_LIBRARIES "-lanl") + + check_symbol_exists( + getaddrinfo_a + netdb.h + HAVE_GETADDRINFO_A) + + cmake_pop_check_state() +endif () + set (HAVE_INOTIFY 0) if (HAVE_INOTIFY_INIT AND HAVE_INOTIFY_ADD_WATCH AND HAVE_INOTIFY_RM_WATCH) set (HAVE_INOTIFY 1) 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 59e68d56e6597..5334e3506f5b5 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs @@ -126,8 +126,26 @@ public static Task GetHostEntryAsync(string hostNameOrAddress, Addr if (NetEventSource.Log.IsEnabled()) { Task t = GetHostEntryCoreAsync(hostNameOrAddress, justReturnParsedIp: false, throwOnIIPAny: true, family, cancellationToken); - t.ContinueWith((t, s) => NetEventSource.Info((string)s!, $"{t.Result} with {((IPHostEntry)t.Result).AddressList.Length} entries"), - hostNameOrAddress, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default); + t.ContinueWith(static (t, s) => + { + string hostNameOrAddress = (string)s!; + + if (t.Status == TaskStatus.RanToCompletion) + { + NetEventSource.Info(hostNameOrAddress, $"{t.Result} with {t.Result.AddressList.Length} entries"); + } + + Exception? ex = t.Exception?.InnerException; + + if (ex is SocketException soex) + { + NetEventSource.Error(hostNameOrAddress, $"{hostNameOrAddress} DNS lookup failed with {soex.ErrorCode}"); + } + else if (ex is OperationCanceledException) + { + NetEventSource.Error(hostNameOrAddress, $"{hostNameOrAddress} DNS lookup was canceled"); + } + }, hostNameOrAddress, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); return t; } else @@ -529,8 +547,6 @@ private static Task GetHostEntryOrAddressesCoreAsync(string hostName, bool justR { if (NameResolutionPal.SupportsGetAddrInfoAsync) { -#pragma warning disable CS0162 // Unreachable code detected -- SupportsGetAddrInfoAsync is a constant on *nix. - // If the OS supports it and 'hostName' is not an IP Address, resolve the name asynchronously // instead of calling the synchronous version in the ThreadPool. // If it fails, we will fall back to ThreadPool as well. @@ -550,7 +566,7 @@ private static Task GetHostEntryOrAddressesCoreAsync(string hostName, bool justR t = NameResolutionPal.GetAddrInfoAsync(hostName, justAddresses, family, cancellationToken); } - // If async resolution started, return task to user. otherwise fall back to sync API on threadpool. + // If async resolution started, return task to user, otherwise fall back to sync API on threadpool. if (t != null) { return t; @@ -631,7 +647,7 @@ private static void ValidateHostName(string hostName) const int MaxHostName = 255; if (hostName.Length > MaxHostName || - (hostName.Length == MaxHostName && hostName[MaxHostName - 1] != '.')) // If 255 chars, the last one must be a dot. + (hostName.Length == MaxHostName && hostName[MaxHostName - 1] != '.')) // If 255 chars, the last one must be a dot. { throw new ArgumentOutOfRangeException(nameof(hostName), SR.Format(SR.net_toolong, nameof(hostName), MaxHostName.ToString(NumberFormatInfo.CurrentInfo))); diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/NameResolutionPal.Unix.cs b/src/libraries/System.Net.NameResolution/src/System/Net/NameResolutionPal.Unix.cs index 00493b29edb49..e64924fe04cbb 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/NameResolutionPal.Unix.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/NameResolutionPal.Unix.cs @@ -1,12 +1,11 @@ // 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.Internals; using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; -using System.Text; using System.Threading; using System.Threading.Tasks; @@ -14,10 +13,145 @@ namespace System.Net { internal static partial class NameResolutionPal { - public const bool SupportsGetAddrInfoAsync = false; + public static bool SupportsGetAddrInfoAsync { get; } = Interop.Sys.PlatformSupportsGetAddrInfoAsync(); - internal static Task? GetAddrInfoAsync(string hostName, bool justAddresses, AddressFamily family, CancellationToken cancellationToken) => - throw new NotSupportedException(); + public static unsafe SocketError TryGetAddrInfo(string name, bool justAddresses, AddressFamily addressFamily, out string? hostName, out string[] aliases, out IPAddress[] addresses, out int nativeErrorCode) + { + Debug.Assert(name is not null); + + if (name.Length == 0) + { + // To match documented behavior on Windows, if an empty string is passed in, use the local host's name. + name = Dns.GetHostName(); + } + + Interop.Sys.HostEntry entry; + int result = Interop.Sys.GetHostEntryForName(name, addressFamily, &entry); + if (result != 0) + { + nativeErrorCode = result; + hostName = name; + aliases = Array.Empty(); + addresses = Array.Empty(); + return GetSocketErrorForNativeError(result); + } + + ParseHostEntry(entry, justAddresses, out hostName, out aliases, out addresses); + nativeErrorCode = 0; + return SocketError.Success; + } + + public static unsafe string? TryGetNameInfo(IPAddress addr, out SocketError socketError, out int nativeErrorCode) + { + byte* buffer = stackalloc byte[Interop.Sys.NI_MAXHOST + 1 /*for null*/]; + + byte isIPv6; + int rawAddressLength; + if (addr.AddressFamily == AddressFamily.InterNetwork) + { + isIPv6 = 0; + rawAddressLength = IPAddressParserStatics.IPv4AddressBytes; + } + else + { + isIPv6 = 1; + rawAddressLength = IPAddressParserStatics.IPv6AddressBytes; + } + + byte* rawAddress = stackalloc byte[rawAddressLength]; + addr.TryWriteBytes(new Span(rawAddress, rawAddressLength), out int bytesWritten); + Debug.Assert(bytesWritten == rawAddressLength); + + int error = Interop.Sys.GetNameInfo( + rawAddress, + (uint)rawAddressLength, + isIPv6, + buffer, + Interop.Sys.NI_MAXHOST, + null, + 0, + Interop.Sys.GetNameInfoFlags.NI_NAMEREQD); + + socketError = GetSocketErrorForNativeError(error); + nativeErrorCode = error; + return socketError == SocketError.Success ? Marshal.PtrToStringAnsi((IntPtr)buffer) : null; + } + + public static string GetHostName() => Interop.Sys.GetHostName(); + + public static unsafe Task? GetAddrInfoAsync(string hostName, bool justAddresses, AddressFamily addressFamily, CancellationToken _) + { + Debug.Assert(hostName is not null); + + if (hostName.Length == 0) + { + // To match documented behavior on Windows, if an empty string is passed in, use the local host's name. + hostName = Dns.GetHostName(); + } + + GetHostEntryForNameContext* context = GetHostEntryForNameContext.AllocateContext(); + + GetHostEntryForNameState state; + try + { + state = new GetHostEntryForNameState(hostName, justAddresses); + context->State = state.CreateHandle(); + } + catch + { + GetHostEntryForNameContext.FreeContext(context); + throw; + } + + int errorCode = Interop.Sys.GetHostEntryForNameAsync(hostName, addressFamily, &context->Result, &GetHostEntryForNameCallback); + + if (errorCode != 0) + { + ProcessResult(GetSocketErrorForNativeError(errorCode), context); + } + + return state.Task; + } + + [UnmanagedCallersOnly] + private static unsafe void GetHostEntryForNameCallback(Interop.Sys.HostEntry* entry, int error) + { + // Can be casted directly to GetHostEntryForNameContext* because the HostEntry is its first field + GetHostEntryForNameContext* context = (GetHostEntryForNameContext*)entry; + + ProcessResult(GetSocketErrorForNativeError(error), context); + } + + private static unsafe void ProcessResult(SocketError errorCode, GetHostEntryForNameContext* context) + { + try + { + GetHostEntryForNameState state = GetHostEntryForNameState.FromHandleAndFree(context->State); + + if (errorCode == SocketError.Success) + { + ParseHostEntry(context->Result, state.JustAddresses, out string? hostName, out string[] aliases, out IPAddress[] addresses); + + state.SetResult(state.JustAddresses + ? (object)addresses + : new IPHostEntry + { + HostName = hostName ?? state.HostName, + Aliases = aliases, + AddressList = addresses + }); + } + else + { + Exception ex = new SocketException((int)errorCode); + state.SetResult(ExceptionDispatchInfo.SetCurrentStackTrace(ex)); + } + } + finally + { + GetHostEntryForNameContext.FreeContext(context); + } + } private static SocketError GetSocketErrorForNativeError(int error) { @@ -48,9 +182,9 @@ private static unsafe void ParseHostEntry(Interop.Sys.HostEntry hostEntry, bool { try { - hostName = !justAddresses && hostEntry.CanonicalName != null ? - Marshal.PtrToStringAnsi((IntPtr)hostEntry.CanonicalName) : - null; + hostName = !justAddresses && hostEntry.CanonicalName != null + ? Marshal.PtrToStringAnsi((IntPtr)hostEntry.CanonicalName) + : null; IPAddress[] localAddresses; if (hostEntry.IPAddressCount == 0) @@ -60,13 +194,13 @@ private static unsafe void ParseHostEntry(Interop.Sys.HostEntry hostEntry, bool else { // getaddrinfo returns multiple entries per address, for each socket type (datagram, stream, etc.). - // Our callers expect just one entry for each address. So we need to deduplicate the results. + // Our callers expect just one entry for each address. So we need to deduplicate the results. // It's important to keep the addresses in order, since they are returned in the order in which // connections should be attempted. // // We assume that the list returned by getaddrinfo is relatively short; after all, the intent is that // the caller may need to attempt to contact every address in the list before giving up on a connection - // attempt. So an O(N^2) algorithm should be fine here. Keep in mind that any "better" algorithm + // attempt. So an O(N^2) algorithm should be fine here. Keep in mind that any "better" algorithm // is likely to involve extra allocations, hashing, etc., and so will probably be more expensive than // this one in the typical (short list) case. @@ -117,66 +251,97 @@ private static unsafe void ParseHostEntry(Interop.Sys.HostEntry hostEntry, bool } } - public static unsafe SocketError TryGetAddrInfo(string name, bool justAddresses, AddressFamily addressFamily, out string? hostName, out string[] aliases, out IPAddress[] addresses, out int nativeErrorCode) + private sealed class GetHostEntryForNameState : IThreadPoolWorkItem { - if (name == "") - { - // To match documented behavior on Windows, if an empty string is passed in, use the local host's name. - name = Dns.GetHostName(); - } + private AsyncTaskMethodBuilder _ipAddressArrayBuilder; + private AsyncTaskMethodBuilder _ipHostEntryBuilder; + private object? _result; - Interop.Sys.HostEntry entry; - int result = Interop.Sys.GetHostEntryForName(name, addressFamily, &entry); - if (result != 0) + public string HostName { get; } + public bool JustAddresses { get; } + + public GetHostEntryForNameState(string hostName, bool justAddresses) { - nativeErrorCode = result; - hostName = name; - aliases = Array.Empty(); - addresses = Array.Empty(); - return GetSocketErrorForNativeError(result); + HostName = hostName; + JustAddresses = justAddresses; + + if (justAddresses) + { + _ipAddressArrayBuilder = AsyncTaskMethodBuilder.Create(); + _ = _ipAddressArrayBuilder.Task; // force initialization + } + else + { + _ipHostEntryBuilder = AsyncTaskMethodBuilder.Create(); + _ = _ipHostEntryBuilder.Task; // force initialization + } } - ParseHostEntry(entry, justAddresses, out hostName, out aliases, out addresses); - nativeErrorCode = 0; - return SocketError.Success; - } + public Task Task => JustAddresses ? _ipAddressArrayBuilder.Task : _ipHostEntryBuilder.Task; - public static unsafe string? TryGetNameInfo(IPAddress addr, out SocketError socketError, out int nativeErrorCode) - { - byte* buffer = stackalloc byte[Interop.Sys.NI_MAXHOST + 1 /*for null*/]; + public void SetResult(object result) + { + // Store the result and then queue this object to the thread pool to actually complete the Tasks, as we + // want to avoid invoking continuations on the OS callback thread. Effectively we're manually + // implementing TaskCreationOptions.RunContinuationsAsynchronously, which we can't use because we're + // using AsyncTaskMethodBuilder, which we're using in order to create either a strongly-typed Task + // or Task without allocating additional objects. + Debug.Assert(result is Exception or IPAddress[] or IPHostEntry); + _result = result; + ThreadPool.UnsafeQueueUserWorkItem(this, preferLocal: false); + } - byte isIPv6; - int rawAddressLength; - if (addr.AddressFamily == AddressFamily.InterNetwork) + void IThreadPoolWorkItem.Execute() { - isIPv6 = 0; - rawAddressLength = IPAddressParserStatics.IPv4AddressBytes; + if (JustAddresses) + { + if (_result is Exception e) + { + _ipAddressArrayBuilder.SetException(e); + } + else + { + _ipAddressArrayBuilder.SetResult((IPAddress[])_result!); + } + } + else + { + if (_result is Exception e) + { + _ipHostEntryBuilder.SetException(e); + } + else + { + _ipHostEntryBuilder.SetResult((IPHostEntry)_result!); + } + } } - else + + public IntPtr CreateHandle() => GCHandle.ToIntPtr(GCHandle.Alloc(this, GCHandleType.Normal)); + + public static GetHostEntryForNameState FromHandleAndFree(IntPtr handle) { - isIPv6 = 1; - rawAddressLength = IPAddressParserStatics.IPv6AddressBytes; + GCHandle gCHandle = GCHandle.FromIntPtr(handle); + var state = (GetHostEntryForNameState)gCHandle.Target!; + gCHandle.Free(); + return state; } + } - byte* rawAddress = stackalloc byte[rawAddressLength]; - addr.TryWriteBytes(new Span(rawAddress, rawAddressLength), out int bytesWritten); - Debug.Assert(bytesWritten == rawAddressLength); + [StructLayout(LayoutKind.Sequential)] + private unsafe struct GetHostEntryForNameContext + { + public Interop.Sys.HostEntry Result; + public IntPtr State; - int error = Interop.Sys.GetNameInfo( - rawAddress, - (uint)rawAddressLength, - isIPv6, - buffer, - Interop.Sys.NI_MAXHOST, - null, - 0, - Interop.Sys.GetNameInfoFlags.NI_NAMEREQD); + public static GetHostEntryForNameContext* AllocateContext() + { + GetHostEntryForNameContext* context = (GetHostEntryForNameContext*)Marshal.AllocHGlobal(sizeof(GetHostEntryForNameContext)); + *context = default; + return context; + } - socketError = GetSocketErrorForNativeError(error); - nativeErrorCode = error; - return socketError == SocketError.Success ? Marshal.PtrToStringAnsi((IntPtr)buffer) : null; + public static void FreeContext(GetHostEntryForNameContext* context) => Marshal.FreeHGlobal((IntPtr)context); } - - public static string GetHostName() => Interop.Sys.GetHostName(); } } diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/LoggingTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/LoggingTest.cs index d8921a09a5581..dacc47eaae2af 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/LoggingTest.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/LoggingTest.cs @@ -5,11 +5,9 @@ using System.Diagnostics.Tracing; using System.Linq; using System.Net.Sockets; -using System.Reflection; using System.Threading.Tasks; using Microsoft.DotNet.XUnitExtensions; using Xunit; -using Xunit.Abstractions; namespace System.Net.NameResolution.Tests { @@ -42,7 +40,7 @@ public void GetHostEntry_InvalidHost_LogsError() try { Dns.GetHostEntry(Configuration.Sockets.InvalidHost); - throw new SkipTestException("GetHostEntry() should fail but it did not."); + throw new SkipTestException("GetHostEntry should fail but it did not."); } catch (SocketException e) when (e.SocketErrorCode == SocketError.HostNotFound) { @@ -53,7 +51,7 @@ public void GetHostEntry_InvalidHost_LogsError() } }); - Assert.True(events.Count() > 0); + Assert.True(events.Count > 0, "events.Count should be > 0"); foreach (EventWrittenEventArgs ev in events) { Assert.True(ev.Payload.Count >= 3); @@ -66,29 +64,31 @@ public void GetHostEntry_InvalidHost_LogsError() [ConditionalFact] [PlatformSpecific(~TestPlatforms.Windows)] // Unreliable on Windows. - public void GetHostEntryAsync_InvalidHost_LogsError() + public async Task GetHostEntryAsync_InvalidHost_LogsError() { using (var listener = new TestEventListener("Private.InternalDiagnostics.System.Net.NameResolution", EventLevel.Error)) { var events = new ConcurrentQueue(); - listener.RunWithCallback(ev => events.Enqueue(ev), () => + await listener.RunWithCallbackAsync(ev => events.Enqueue(ev), async () => { try { - Dns.GetHostEntryAsync(Configuration.Sockets.InvalidHost).GetAwaiter().GetResult(); - throw new SkipTestException("GetHostEntry() should fail but it did not."); + await Dns.GetHostEntryAsync(Configuration.Sockets.InvalidHost).ConfigureAwait(false); + throw new SkipTestException("GetHostEntryAsync should fail but it did not."); } catch (SocketException e) when (e.SocketErrorCode == SocketError.HostNotFound) { + // Wait a bit to let the event source write it's log + await Task.Delay(100).ConfigureAwait(false); } catch (Exception e) { - throw new SkipTestException($"GetHostEntry failed unexpectedly: {e.Message}"); + throw new SkipTestException($"GetHostEntryAsync failed unexpectedly: {e.Message}"); } - }); + }).ConfigureAwait(false); - Assert.True(events.Count() > 0); + Assert.True(events.Count > 0, "events.Count should be > 0"); foreach (EventWrittenEventArgs ev in events) { Assert.True(ev.Payload.Count >= 3); diff --git a/src/libraries/System.Net.NameResolution/tests/PalTests/NameResolutionPalTests.cs b/src/libraries/System.Net.NameResolution/tests/PalTests/NameResolutionPalTests.cs index 3c5a050a42e52..6bb0cf6cf5c60 100644 --- a/src/libraries/System.Net.NameResolution/tests/PalTests/NameResolutionPalTests.cs +++ b/src/libraries/System.Net.NameResolution/tests/PalTests/NameResolutionPalTests.cs @@ -3,6 +3,9 @@ using System.IO; using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -10,7 +13,7 @@ namespace System.Net.NameResolution.PalTests { public class NameResolutionPalTests { - private ITestOutputHelper _output; + private readonly ITestOutputHelper _output; public NameResolutionPalTests(ITestOutputHelper output) { @@ -26,18 +29,35 @@ private void LogUnixInfo() _output.WriteLine("------"); } - [Fact] - public void HostName_NotNull() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void TryGetAddrInfo_LocalHost(bool justAddresses) { - Assert.NotNull(NameResolutionPal.GetHostName()); + SocketError error = NameResolutionPal.TryGetAddrInfo("localhost", justAddresses, AddressFamily.Unspecified, out string hostName, out string[] aliases, out IPAddress[] addresses, out int nativeErrorCode); + Assert.Equal(SocketError.Success, error); + if (!justAddresses) + { + Assert.NotNull(hostName); + } + Assert.NotNull(aliases); + Assert.NotNull(addresses); + Assert.True(addresses.Length > 0); } [Theory] [InlineData(false)] [InlineData(true)] - public void TryGetAddrInfo_LocalHost(bool justAddresses) + public void TryGetAddrInfo_EmptyHost(bool justAddresses) { - SocketError error = NameResolutionPal.TryGetAddrInfo("localhost", justAddresses, AddressFamily.Unspecified, out string hostName, out string[] aliases, out IPAddress[] addresses, out int nativeErrorCode); + SocketError error = NameResolutionPal.TryGetAddrInfo("", justAddresses, AddressFamily.Unspecified, out string hostName, out string[] aliases, out IPAddress[] addresses, out int nativeErrorCode); + if (error == SocketError.HostNotFound && !OperatingSystem.IsWindows()) + { + // On Unix, we are not guaranteed to be able to resove the local host. The ability to do so depends on the + // machine configurations, which varies by distro and is often inconsistent. + return; + } + Assert.Equal(SocketError.Success, error); if (!justAddresses) { @@ -45,6 +65,7 @@ public void TryGetAddrInfo_LocalHost(bool justAddresses) } Assert.NotNull(aliases); Assert.NotNull(addresses); + Assert.True(addresses.Length > 0); } [Theory] @@ -57,7 +78,7 @@ public void TryGetAddrInfo_HostName(bool justAddresses) Assert.NotNull(hostName); SocketError error = NameResolutionPal.TryGetAddrInfo(hostName, justAddresses, AddressFamily.Unspecified, out hostName, out string[] aliases, out IPAddress[] addresses, out int nativeErrorCode); - if (error == SocketError.HostNotFound && (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())) + if (error == SocketError.HostNotFound && !OperatingSystem.IsWindows()) { // On Unix, we are not guaranteed to be able to resove the local host. The ability to do so depends on the // machine configurations, which varies by distro and is often inconsistent. @@ -71,6 +92,33 @@ public void TryGetAddrInfo_HostName(bool justAddresses) } Assert.NotNull(aliases); Assert.NotNull(addresses); + Assert.True(addresses.Length > 0); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void TryGetAddrInfo_ExternalHost(bool justAddresses) + { + string hostName = "microsoft.com"; + + SocketError error = NameResolutionPal.TryGetAddrInfo(hostName, justAddresses, AddressFamily.Unspecified, out hostName, out string[] aliases, out IPAddress[] addresses, out _); + Assert.Equal(SocketError.Success, error); + Assert.NotNull(aliases); + Assert.NotNull(addresses); + Assert.True(addresses.Length > 0); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + [OuterLoop("Uses external server")] + public void TryGetAddrInfo_UnknownHost(bool justAddresses) + { + SocketError error = NameResolutionPal.TryGetAddrInfo("test.123", justAddresses, AddressFamily.Unspecified, out string? _, out string[] _, out IPAddress[] _, out int nativeErrorCode); + + Assert.Equal(SocketError.HostNotFound, error); + Assert.NotEqual(0, nativeErrorCode); } [Fact] @@ -146,19 +194,6 @@ public void TryGetAddrInfo_HostName_TryGetNameInfo() Assert.NotNull(name); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void TryGetAddrInfo_ExternalHost(bool justAddresses) - { - string hostName = "microsoft.com"; - - SocketError error = NameResolutionPal.TryGetAddrInfo(hostName, justAddresses, AddressFamily.Unspecified, out hostName, out string[] aliases, out IPAddress[] addresses, out _); - Assert.Equal(SocketError.Success, error); - Assert.NotNull(aliases); - Assert.NotNull(addresses); - } - [Theory] [InlineData(false)] [InlineData(true)] @@ -201,11 +236,192 @@ public void TryGetNameInfo_LocalHost_IPv6_TryGetAddrInfo(bool justAddresses) Assert.NotNull(addresses); } + [Fact] + public void HostName_NotNull() + { + Assert.NotNull(NameResolutionPal.GetHostName()); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task GetAddrInfoAsync_LocalHost(bool justAddresses) + { + if (!NameResolutionPal.SupportsGetAddrInfoAsync) + { + return; + } + + if (justAddresses) + { + IPAddress[] addresses = await ((Task)NameResolutionPal.GetAddrInfoAsync("localhost", justAddresses, AddressFamily.Unspecified, CancellationToken.None)).ConfigureAwait(false); + + Assert.NotNull(addresses); + Assert.True(addresses.Length > 0); + } + else + { + IPHostEntry hostEntry = await ((Task)NameResolutionPal.GetAddrInfoAsync("localhost", justAddresses, AddressFamily.Unspecified, CancellationToken.None)).ConfigureAwait(false); + + Assert.NotNull(hostEntry); + Assert.True(hostEntry.AddressList.Length > 0); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + [OuterLoop("Uses external server")] + public async Task GetAddrInfoAsync_EmptyHost(bool justAddresses) + { + if (!NameResolutionPal.SupportsGetAddrInfoAsync) + { + return; + } + + Task task = NameResolutionPal.GetAddrInfoAsync("", justAddresses, AddressFamily.Unspecified, CancellationToken.None); + + try + { + await task.ConfigureAwait(false); + } + catch (SocketException ex) + { + SocketError error = ex.SocketErrorCode; + + if (error == SocketError.HostNotFound && !OperatingSystem.IsWindows()) + { + // On Unix, we are not guaranteed to be able to resove the local host. The ability to do so depends on the + // machine configurations, which varies by distro and is often inconsistent. + return; + } + + throw; + } + + if (justAddresses) + { + IPAddress[] addresses = ((Task)task).Result; + + Assert.NotNull(addresses); + Assert.True(addresses.Length > 0); + } + else + { + IPHostEntry hostEntry = ((Task)task).Result; + + Assert.NotNull(hostEntry); + Assert.True(hostEntry.AddressList.Length > 0); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + [OuterLoop("Uses external server")] + public async Task GetAddrInfoAsync_HostName(bool justAddresses) + { + if (!NameResolutionPal.SupportsGetAddrInfoAsync) + { + return; + } + + string hostName = NameResolutionPal.GetHostName(); + Assert.NotNull(hostName); + + Task task = NameResolutionPal.GetAddrInfoAsync(hostName, justAddresses, AddressFamily.Unspecified, CancellationToken.None); + + try + { + await task.ConfigureAwait(false); + } + catch (SocketException ex) + { + SocketError error = ex.SocketErrorCode; + + if (error == SocketError.HostNotFound && !OperatingSystem.IsWindows()) + { + // On Unix, we are not guaranteed to be able to resove the local host. The ability to do so depends on the + // machine configurations, which varies by distro and is often inconsistent. + return; + } + + throw; + } + + if (justAddresses) + { + IPAddress[] addresses = ((Task)task).Result; + + Assert.NotNull(addresses); + Assert.True(addresses.Length > 0); + } + else + { + IPHostEntry hostEntry = ((Task)task).Result; + + Assert.NotNull(hostEntry); + Assert.True(hostEntry.AddressList.Length > 0); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task GetAddrInfoAsync_ExternalHost(bool justAddresses) + { + if (!NameResolutionPal.SupportsGetAddrInfoAsync) + { + return; + } + + const string hostName = "microsoft.com"; + + if (!NameResolutionPal.SupportsGetAddrInfoAsync) + { + return; + } + + if (justAddresses) + { + IPAddress[] addresses = await ((Task)NameResolutionPal.GetAddrInfoAsync(hostName, justAddresses, AddressFamily.Unspecified, CancellationToken.None)).ConfigureAwait(false); + + Assert.NotNull(addresses); + Assert.True(addresses.Length > 0); + } + else + { + IPHostEntry hostEntry = await ((Task)NameResolutionPal.GetAddrInfoAsync(hostName, justAddresses, AddressFamily.Unspecified, CancellationToken.None)).ConfigureAwait(false); + + Assert.NotNull(hostEntry); + Assert.True(hostEntry.AddressList.Length > 0); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + [OuterLoop("Uses external server")] + public async Task GetAddrInfoAsync_UnknownHost(bool justAddresses) + { + if (!NameResolutionPal.SupportsGetAddrInfoAsync) + { + return; + } + + const string hostName = "test.123"; + + SocketException socketException = await Assert.ThrowsAnyAsync(() => NameResolutionPal.GetAddrInfoAsync(hostName, justAddresses, AddressFamily.Unspecified, CancellationToken.None)).ConfigureAwait(false); + SocketError socketError = socketException.SocketErrorCode; + + Assert.Equal(SocketError.HostNotFound, socketError); + } + [Fact] [PlatformSpecific(TestPlatforms.AnyUnix)] public void Exception_HostNotFound_Success() { - var ex = new SocketException((int)SocketError.HostNotFound); + var ex = new SocketException((int)SocketError.HostNotFound); Assert.Equal(-1, ex.Message.IndexOf("Device")); }