diff --git a/src/libraries/Common/src/Interop/OSX/Interop.Dnssd.cs b/src/libraries/Common/src/Interop/OSX/Interop.Dnssd.cs new file mode 100644 index 00000000000000..63bdf20cf146b2 --- /dev/null +++ b/src/libraries/Common/src/Interop/OSX/Interop.Dnssd.cs @@ -0,0 +1,75 @@ +// 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; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +internal static partial class Interop +{ + internal static unsafe partial class Dnssd + { + internal const uint kDNSServiceFlagsMoreComing = 0x1; + internal const uint kDNSServiceFlagsAdd = 0x2; + internal const uint kDNSServiceFlagsReturnIntermediates = 0x1000; + internal const uint kDNSServiceFlagsTimeout = 0x10000; + + internal const int kDNSServiceErr_NoError = 0; + internal const int kDNSServiceErr_Unknown = -65537; + internal const int kDNSServiceErr_NoSuchName = -65538; + internal const int kDNSServiceErr_NoMemory = -65539; + internal const int kDNSServiceErr_BadParam = -65540; + internal const int kDNSServiceErr_Unsupported = -65544; + internal const int kDNSServiceErr_Refused = -65553; + internal const int kDNSServiceErr_NoSuchRecord = -65554; + internal const int kDNSServiceErr_ServiceNotRunning = -65563; + internal const int kDNSServiceErr_Timeout = -65568; + internal const int kDNSServiceErr_DefunctConnection = -65569; + internal const int kDNSServiceErr_PolicyDenied = -65570; + internal const int kDNSServiceErr_NotPermitted = -65571; + + internal const ushort kDNSServiceClass_IN = 1; + + internal const ushort kDNSServiceType_A = 1; + internal const ushort kDNSServiceType_NS = 2; + internal const ushort kDNSServiceType_CNAME = 5; + internal const ushort kDNSServiceType_PTR = 12; + internal const ushort kDNSServiceType_MX = 15; + internal const ushort kDNSServiceType_TXT = 16; + internal const ushort kDNSServiceType_AAAA = 28; + internal const ushort kDNSServiceType_SRV = 33; + + [LibraryImport(Libraries.libSystem, StringMarshalling = StringMarshalling.Utf8)] + internal static partial int DNSServiceQueryRecord( + out SafeDnsServiceHandle sdRef, + uint flags, + uint interfaceIndex, + string fullname, + ushort rrtype, + ushort rrclass, + delegate* unmanaged[Cdecl] callBack, + IntPtr context); + + [LibraryImport(Libraries.libSystem)] + internal static partial int DNSServiceRefSockFD(SafeDnsServiceHandle sdRef); + + [LibraryImport(Libraries.libSystem)] + internal static partial int DNSServiceProcessResult(SafeDnsServiceHandle sdRef); + + [LibraryImport(Libraries.libSystem)] + internal static partial void DNSServiceRefDeallocate(IntPtr sdRef); + } +} + +internal sealed class SafeDnsServiceHandle : SafeHandleZeroOrMinusOneIsInvalid +{ + public SafeDnsServiceHandle() : base(ownsHandle: true) { } + + protected override bool ReleaseHandle() + { + Interop.Dnssd.DNSServiceRefDeallocate(handle); + return true; + } +} 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 b4f741a9d8e99e..ffba8340fa7889 100644 --- a/src/libraries/System.Net.NameResolution/src/System.Net.NameResolution.csproj +++ b/src/libraries/System.Net.NameResolution/src/System.Net.NameResolution.csproj @@ -1,7 +1,7 @@ - $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-unix;$(NetCoreAppCurrent)-browser;$(NetCoreAppCurrent)-wasi;$(NetCoreAppCurrent) + $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-unix;$(NetCoreAppCurrent)-osx;$(NetCoreAppCurrent)-browser;$(NetCoreAppCurrent)-wasi;$(NetCoreAppCurrent) true false @@ -86,17 +86,52 @@ Link="Common\Interop\Windows\WinSock\Interop.GetAddrInfoExW.cs" /> - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -177,7 +216,7 @@ - + diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsRecordParsing.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsRecordParsing.cs index ffdbb283e281ee..ed4995a82b2ffb 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsRecordParsing.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsRecordParsing.cs @@ -137,6 +137,8 @@ public bool MoveNext() } public readonly DnsTxtEnumerator GetEnumerator() => this; + + public readonly bool IsValid => _remaining.IsEmpty; } internal readonly ref struct DnsPtrRecordData diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs index d48114ed6ded91..263ec3878305cb 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolver.cs @@ -518,6 +518,13 @@ private static TimeSpan MinNonZero(TimeSpan x, TimeSpan y) private static void ValidateName(string name) { ArgumentException.ThrowIfNullOrEmpty(name); + // Every underlying resolver (Windows DnsQueryEx, macOS DNSServiceQueryRecord, + // and the managed stub resolver on Linux) passes the name to native code as a + // null-terminated string, so an embedded NUL would silently truncate the query. + if (name.Contains('\0')) + { + throw new ArgumentException(SR.net_hostname_invalid_character, nameof(name)); + } } /// diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.OSX.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.OSX.cs new file mode 100644 index 00000000000000..d1a65370eec2c6 --- /dev/null +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsResolverPal.OSX.cs @@ -0,0 +1,310 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net +{ + // macOS DNS resolver implementation. Queries without explicit servers use + // DNSServiceQueryRecord so macOS resolver policy remains authoritative; queries + // with explicit servers use the unchanged managed PAL overloads. The array overloads + // ensure DnsResolver calls route here first; casting selects the managed IList overload. + internal static partial class DnsResolverPal + { + public static Task> ResolveAddresses(IPEndPoint[] servers, bool async, string name, AddressFamily addressFamily, CancellationToken cancellationToken) + => servers.Length == 0 + ? Query(servers, async, name, AddressFamilyToQueryType(addressFamily), cancellationToken, DnsSdRecordParsing.TryParseAddress) + : ResolveAddresses((IList)servers, async, name, addressFamily, cancellationToken); + + public static Task> ResolveSrv(IPEndPoint[] servers, bool async, string name, CancellationToken cancellationToken) + => servers.Length == 0 + ? Query(servers, async, name, Interop.Dnssd.kDNSServiceType_SRV, cancellationToken, DnsSdRecordParsing.TryParseSrv) + : ResolveSrv((IList)servers, async, name, cancellationToken); + + public static Task> ResolveMx(IPEndPoint[] servers, bool async, string name, CancellationToken cancellationToken) + => servers.Length == 0 + ? Query(servers, async, name, Interop.Dnssd.kDNSServiceType_MX, cancellationToken, DnsSdRecordParsing.TryParseMx) + : ResolveMx((IList)servers, async, name, cancellationToken); + + public static Task> ResolveTxt(IPEndPoint[] servers, bool async, string name, CancellationToken cancellationToken) + => servers.Length == 0 + ? Query(servers, async, name, Interop.Dnssd.kDNSServiceType_TXT, cancellationToken, DnsSdRecordParsing.TryParseTxt) + : ResolveTxt((IList)servers, async, name, cancellationToken); + + public static Task> ResolveCName(IPEndPoint[] servers, bool async, string name, CancellationToken cancellationToken) + => servers.Length == 0 + ? Query(servers, async, name, Interop.Dnssd.kDNSServiceType_CNAME, cancellationToken, DnsSdRecordParsing.TryParseCName) + : ResolveCName((IList)servers, async, name, cancellationToken); + + public static Task> ResolvePtr(IPEndPoint[] servers, bool async, string name, CancellationToken cancellationToken) + => servers.Length == 0 + ? Query(servers, async, name, Interop.Dnssd.kDNSServiceType_PTR, cancellationToken, DnsSdRecordParsing.TryParsePtr) + : ResolvePtr((IList)servers, async, name, cancellationToken); + + public static Task> ResolveNs(IPEndPoint[] servers, bool async, string name, CancellationToken cancellationToken) + => servers.Length == 0 + ? Query(servers, async, name, Interop.Dnssd.kDNSServiceType_NS, cancellationToken, DnsSdRecordParsing.TryParseNs) + : ResolveNs((IList)servers, async, name, cancellationToken); + + private static ushort AddressFamilyToQueryType(AddressFamily addressFamily) => + addressFamily switch + { + AddressFamily.InterNetwork => Interop.Dnssd.kDNSServiceType_A, + AddressFamily.InterNetworkV6 => Interop.Dnssd.kDNSServiceType_AAAA, + _ => throw new ArgumentException(SR.net_dns_unsupported_address_family, nameof(addressFamily)), + }; + + private static Task> Query( + IPEndPoint[] servers, + bool async, + string name, + ushort queryType, + CancellationToken cancellationToken, + TryParseDnsSdRecord tryParse) + { + ValidateServers(servers); + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled>(cancellationToken); + } + + return QueryCore(name, queryType, async, cancellationToken, tryParse); + } + + // The Linux managed PAL owns the DNS UDP/TCP socket end-to-end and reads bytes via + // Socket.ReceiveAsync; async there is a plain socket read. Here mDNSResponder (a system + // daemon) owns the socket. Its client library exposes only an fd via DNSServiceRefSockFD, + // and the actual DNS wire read + parse + callback dispatch happen inside + // DNSServiceProcessResult. So on async we await readability on the fd and then call + // DNSServiceProcessResult synchronously to consume it. DNSServiceProcessResult blocks + // until data is available for synchronous callers, so no separate polling is needed. + private static async Task> QueryCore( + string name, + ushort queryType, + bool async, + CancellationToken cancellationToken, + TryParseDnsSdRecord tryParse) + { + DnsSdQueryResult raw = await QueryRecord(name, queryType, async, cancellationToken).ConfigureAwait(false); + return BuildResult(raw, queryType, tryParse); + } + + private static DnsResult BuildResult( + DnsSdQueryResult raw, + ushort queryType, + TryParseDnsSdRecord tryParse) + { + if (raw.ResponseCode != DnsResponseCode.NoError) + { + return new DnsResult(raw.ResponseCode, null, TimeSpan.Zero); + } + + List records = new(); + foreach (DnsSdRecord rawRecord in raw.Records) + { + if (rawRecord.Type == queryType && tryParse(rawRecord, out TRecord parsed)) + { + records.Add(parsed); + } + } + + return new DnsResult(DnsResponseCode.NoError, records, TimeSpan.Zero); + } + + private static async Task QueryRecord(string name, ushort queryType, bool async, CancellationToken cancellationToken) + { + DnsSdQueryState state = new(queryType); + GCHandle stateHandle = new(state); + SafeDnsServiceHandle? dnsService = null; + + try + { + int status = StartQuery(name, queryType, stateHandle, out dnsService); + if (status != Interop.Dnssd.kDNSServiceErr_NoError) + { + return DnsSdQueryResult.FromStatus(status); + } + + int fileDescriptor = Interop.Dnssd.DNSServiceRefSockFD(dnsService); + if (fileDescriptor < 0) + { + return DnsSdQueryResult.FromStatus(Interop.Dnssd.kDNSServiceErr_DefunctConnection); + } + + using DnsSocket? readinessSocket = async ? new DnsSocket((IntPtr)fileDescriptor) : null; + byte[] readinessBuffer = new byte[1]; + + while (!state.IsComplete) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (async) + { + await readinessSocket!.ReceiveAsync(readinessBuffer, peek: true, cancellationToken).ConfigureAwait(false); + } + int processStatus = Interop.Dnssd.DNSServiceProcessResult(dnsService); + if (processStatus != Interop.Dnssd.kDNSServiceErr_NoError) + { + state.SetError(processStatus); + } + } + + return state.ToResult(); + } + finally + { + dnsService?.Dispose(); + stateHandle.Dispose(); + } + } + + private static unsafe int StartQuery(string name, ushort queryType, GCHandle stateHandle, out SafeDnsServiceHandle serviceRef) => + Interop.Dnssd.DNSServiceQueryRecord( + out serviceRef, + flags: Interop.Dnssd.kDNSServiceFlagsReturnIntermediates | Interop.Dnssd.kDNSServiceFlagsTimeout, + interfaceIndex: 0, + fullname: name, + rrtype: queryType, + rrclass: Interop.Dnssd.kDNSServiceClass_IN, + callBack: &QueryRecordCallback, + context: GCHandle.ToIntPtr(stateHandle)); + +#pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#pragma warning restore CS3016 + private static unsafe void QueryRecordCallback( + IntPtr sdRef, + uint flags, + uint interfaceIndex, + int errorCode, + byte* fullname, + ushort rrtype, + ushort rrclass, + ushort rdlen, + void* rdata, + uint ttl, + IntPtr context) + { + DnsSdQueryState? state = null; + try + { + state = GCHandle.FromIntPtr(context).Target; + state.OnRecord(flags, interfaceIndex, errorCode, rrtype, rrclass, rdlen, rdata, ttl); + } + catch (Exception ex) + { + state?.SetException(ex); + } + } + + private readonly struct DnsSdQueryResult + { + public DnsResponseCode ResponseCode { get; } + public IReadOnlyList Records { get; } + + public DnsSdQueryResult(DnsResponseCode responseCode, IReadOnlyList records) + { + ResponseCode = responseCode; + Records = records; + } + + public static DnsSdQueryResult FromStatus(int status) => + new(MapDnsServiceErrorToResponseCode(status), Array.Empty()); + } + + private sealed unsafe class DnsSdQueryState + { + private readonly ushort _requestedType; + private readonly List _records = new(); + private int _status = Interop.Dnssd.kDNSServiceErr_NoError; + private Exception? _exception; + + public DnsSdQueryState(ushort requestedType) + { + _requestedType = requestedType; + } + + public bool IsComplete { get; private set; } + + public void SetError(int status) + { + _status = status; + IsComplete = true; + } + + public void SetException(Exception exception) + { + _exception ??= exception; + IsComplete = true; + } + + public void OnRecord(uint flags, uint interfaceIndex, int errorCode, ushort rrtype, ushort rrclass, ushort rdlen, void* rdata, uint ttl) + { + if (errorCode != Interop.Dnssd.kDNSServiceErr_NoError) + { + SetError(errorCode); + return; + } + + if (rrclass != Interop.Dnssd.kDNSServiceClass_IN || rrtype != _requestedType) + { + return; + } + + if ((flags & Interop.Dnssd.kDNSServiceFlagsAdd) != 0 && rdata != null) + { + // Best-effort TTL: DNS-SD may return the original TTL for cached answers. + _records.Add(new DnsSdRecord(rrtype, new ReadOnlySpan(rdata, rdlen).ToArray(), ttl, interfaceIndex)); + } + + if ((flags & Interop.Dnssd.kDNSServiceFlagsMoreComing) == 0) + { + IsComplete = true; + } + } + + public DnsSdQueryResult ToResult() + { + Exception? exception = _exception; + if (exception is not null) + { + ExceptionDispatchInfo.Throw(exception); + } + + DnsResponseCode responseCode = MapDnsServiceErrorToResponseCode(_status); + + return new DnsSdQueryResult(responseCode, _records); + } + } + + + private static DnsResponseCode MapDnsServiceErrorToResponseCode(int status) => + status switch + { + Interop.Dnssd.kDNSServiceErr_NoError => DnsResponseCode.NoError, + Interop.Dnssd.kDNSServiceErr_NoSuchName => DnsResponseCode.NxDomain, + // DNSServiceQueryRecord reports NODATA as NoSuchRecord, and mDNSResponder + // also uses that code for NXDOMAIN in practice. The callback does not expose + // the authority section needed to distinguish them, so surface the collapsed + // negative result as a successful response with no records. + Interop.Dnssd.kDNSServiceErr_NoSuchRecord => DnsResponseCode.NoError, + // With kDNSServiceFlagsTimeout, DNSServiceQueryRecord uses Timeout as the + // terminal callback when the query times out. + Interop.Dnssd.kDNSServiceErr_Timeout => DnsResponseCode.ServerFailure, + Interop.Dnssd.kDNSServiceErr_BadParam => DnsResponseCode.FormatError, + Interop.Dnssd.kDNSServiceErr_Unsupported => DnsResponseCode.NotImplemented, + Interop.Dnssd.kDNSServiceErr_Refused => DnsResponseCode.Refused, + Interop.Dnssd.kDNSServiceErr_PolicyDenied => DnsResponseCode.Refused, + Interop.Dnssd.kDNSServiceErr_NotPermitted => DnsResponseCode.Refused, + _ => DnsResponseCode.ServerFailure, + }; + } + +} diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsSdRecordParsing.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsSdRecordParsing.cs new file mode 100644 index 00000000000000..d6b36ad7855520 --- /dev/null +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsSdRecordParsing.cs @@ -0,0 +1,153 @@ +// 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.Text; + +namespace System.Net +{ + // Rdata returned by macOS mDNSResponder (DNSServiceQueryRecord). + internal readonly struct DnsSdRecord + { + public ushort Type { get; } + public byte[] Data { get; } + public uint Ttl { get; } + public uint InterfaceIndex { get; } + + public DnsSdRecord(ushort type, byte[] data, uint ttl, uint interfaceIndex) + { + Type = type; + Data = data; + Ttl = ttl; + InterfaceIndex = interfaceIndex; + } + } + + internal delegate bool TryParseDnsSdRecord(DnsSdRecord record, out TRecord parsed); + + // Parses rdata returned by macOS mDNSResponder into typed DNS records. Kept separate + // from DnsResolverPal.OSX so it can be unit-tested without reaching into PAL internals. + internal static class DnsSdRecordParsing + { + public static bool TryParseAddress(DnsSdRecord record, out AddressRecord parsed) + { + if (record.Data.Length == 4 || record.Data.Length == 16) + { + IPAddress address = new IPAddress(record.Data); + if (address.IsIPv6LinkLocal) + { + address.ScopeId = record.InterfaceIndex; + } + + parsed = new AddressRecord(address, TimeSpan.FromSeconds(record.Ttl)); + return true; + } + + parsed = default; + return false; + } + + public static bool TryParseSrv(DnsSdRecord record, out SrvRecord parsed) + { + DnsRecord dnsRecord = ToDnsRecord(record); + if (dnsRecord.TryParseSrvRecord(out DnsSrvRecordData srv)) + { + parsed = new SrvRecord( + srv.Target.ToString(), + srv.Port, + srv.Priority, + srv.Weight, + TimeSpan.FromSeconds(record.Ttl), + // DNSServiceQueryRecord exposes only the queried record's rdata, not + // additional-section glue A/AAAA records. + null); + return true; + } + + parsed = default; + return false; + } + + public static bool TryParseMx(DnsSdRecord record, out MxRecord parsed) + { + DnsRecord dnsRecord = ToDnsRecord(record); + if (dnsRecord.TryParseMxRecord(out DnsMxRecordData mx)) + { + parsed = new MxRecord(mx.Exchange.ToString(), mx.Preference, TimeSpan.FromSeconds(record.Ttl)); + return true; + } + + parsed = default; + return false; + } + + public static bool TryParseTxt(DnsSdRecord record, out TxtRecord parsed) + { + DnsRecord dnsRecord = ToDnsRecord(record); + if (!dnsRecord.TryParseTxtRecord(out DnsTxtRecordData txt)) + { + parsed = default; + return false; + } + + List values = new(); + DnsTxtEnumerator enumerator = txt.EnumerateStrings(); + while (enumerator.MoveNext()) + { + values.Add(Encoding.UTF8.GetString(enumerator.Current)); + } + + if (!enumerator.IsValid) + { + parsed = default; + return false; + } + + parsed = new TxtRecord(values, TimeSpan.FromSeconds(record.Ttl)); + return true; + } + + public static bool TryParseCName(DnsSdRecord record, out CNameRecord parsed) + { + DnsRecord dnsRecord = ToDnsRecord(record); + if (dnsRecord.TryParseCNameRecord(out DnsCNameRecordData cname)) + { + parsed = new CNameRecord(cname.CName.ToString(), TimeSpan.FromSeconds(record.Ttl)); + return true; + } + + parsed = default; + return false; + } + + public static bool TryParsePtr(DnsSdRecord record, out PtrRecord parsed) + { + DnsRecord dnsRecord = ToDnsRecord(record); + if (dnsRecord.TryParsePtrRecord(out DnsPtrRecordData ptr)) + { + parsed = new PtrRecord(ptr.Name.ToString(), TimeSpan.FromSeconds(record.Ttl)); + return true; + } + + parsed = default; + return false; + } + + public static bool TryParseNs(DnsSdRecord record, out NsRecord parsed) + { + DnsRecord dnsRecord = ToDnsRecord(record); + if (dnsRecord.TryParseNsRecord(out DnsNsRecordData ns)) + { + parsed = new NsRecord(ns.Name.ToString(), TimeSpan.FromSeconds(record.Ttl)); + return true; + } + + parsed = default; + return false; + } + + private static DnsRecord ToDnsRecord(DnsSdRecord record) => + new DnsRecord(default, (DnsRecordType)record.Type, DnsRecordClass.Internet, + record.Ttl, record.Data, record.Data, 0); + } +} diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/DnsSocket.cs b/src/libraries/System.Net.NameResolution/src/System/Net/DnsSocket.cs index f428c5f21d8d65..bfa42cead4c4be 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/DnsSocket.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/DnsSocket.cs @@ -1,152 +1,106 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Net.Sockets; +using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; namespace System.Net { - // Thin wrapper over System.Net.Sockets.Socket accessed via reflection. + // Thin wrapper over System.Net.Sockets.Socket without a static assembly reference. // // System.Net.Sockets depends on System.Net.NameResolution (Socket.Connect(host, port) // resolves names through Dns), so NameResolution cannot statically reference the Sockets // assembly without introducing a cycle in the shared-framework closure. The managed DNS - // stub resolver still needs raw UDP/TCP sockets, so it reaches Socket through reflection; - // the assembly is resolved from the shared framework at runtime. SocketException, + // stub resolver still needs raw UDP/TCP sockets, so it reaches Socket through type-name + // accessors; the assembly is resolved from the shared framework at runtime. SocketException, // SocketError and AddressFamily live in System.Net.Primitives and are used directly. // - // Instance operations are exposed through delegates bound to the underlying Socket so that - // exceptions (e.g. SocketException) propagate to callers directly instead of being wrapped - // in a TargetInvocationException. internal sealed class DnsSocket : IDisposable { - private sealed class SocketReflection - { - public ConstructorInfo Constructor = null!; - public MethodInfo ConnectAsyncMethod = null!; - public MethodInfo SendAsyncMethod = null!; - public MethodInfo ReceiveAsyncMethod = null!; - public MethodInfo ConnectMethod = null!; - public MethodInfo SendMethod = null!; - public MethodInfo ReceiveMethod = null!; - public MethodInfo BeginConnectMethod = null!; - public MethodInfo EndConnectMethod = null!; - public MethodInfo DisposeMethod = null!; - public MethodInfo SetSendTimeoutMethod = null!; - public MethodInfo SetReceiveTimeoutMethod = null!; - public object SocketTypeDgram = null!; - public object SocketTypeStream = null!; - public object ProtocolTypeUdp = null!; - public object ProtocolTypeTcp = null!; - } - - private static readonly SocketReflection s_reflection = CreateReflection(); - - private delegate int SendSpanDelegate(ReadOnlySpan buffer); - private delegate int ReceiveSpanDelegate(Span buffer); - - private readonly Func _connectAsync; - private readonly Func, CancellationToken, ValueTask> _sendAsync; - private readonly Func, CancellationToken, ValueTask> _receiveAsync; - private readonly Action _connect; - private readonly SendSpanDelegate _send; - private readonly ReceiveSpanDelegate _receive; - private readonly Func _beginConnect; - private readonly Action _endConnect; - private readonly Action _setSendTimeout; - private readonly Action _setReceiveTimeout; - private readonly Action _dispose; + private const string SocketTypeName = "System.Net.Sockets.Socket, System.Net.Sockets"; + private const string SocketTypeEnumName = "System.Net.Sockets.SocketType, System.Net.Sockets"; + private const string ProtocolTypeEnumName = "System.Net.Sockets.ProtocolType, System.Net.Sockets"; + private const string SafeSocketHandleTypeName = "System.Net.Sockets.SafeSocketHandle, System.Net.Sockets"; + private const string SocketFlagsTypeName = "System.Net.Sockets.SocketFlags, System.Net.Sockets"; + + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] + private static readonly Type s_socketType = Type.GetType(SocketTypeName, throwOnError: true)!; + private static readonly Type s_socketTypeEnum = Type.GetType(SocketTypeEnumName, throwOnError: true)!; + private static readonly Type s_protocolTypeEnum = Type.GetType(ProtocolTypeEnumName, throwOnError: true)!; + // UnsafeAccessorType cannot represent the SocketType and ProtocolType value-type + // parameters without referencing System.Net.Sockets, which would create a cycle. + private static readonly ConstructorInfo s_socketConstructor = + s_socketType.GetConstructor(new[] { typeof(AddressFamily), s_socketTypeEnum, s_protocolTypeEnum })!; + private static readonly object s_socketTypeDgram = Enum.Parse(Type.GetType(SocketTypeEnumName, throwOnError: true)!, "Dgram"); + private static readonly object s_socketTypeStream = Enum.Parse(Type.GetType(SocketTypeEnumName, throwOnError: true)!, "Stream"); + private static readonly object s_protocolTypeUdp = Enum.Parse(Type.GetType(ProtocolTypeEnumName, throwOnError: true)!, "Udp"); + private static readonly object s_protocolTypeTcp = Enum.Parse(Type.GetType(ProtocolTypeEnumName, throwOnError: true)!, "Tcp"); + private static readonly object s_socketFlagsPeek = Enum.Parse(Type.GetType(SocketFlagsTypeName, throwOnError: true)!, "Peek"); [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicProperties, "System.Net.Sockets.Socket", "System.Net.Sockets")] - private static SocketReflection CreateReflection() - { - Type socketType = Type.GetType("System.Net.Sockets.Socket, System.Net.Sockets", throwOnError: true)!; - Type socketTypeEnum = Type.GetType("System.Net.Sockets.SocketType, System.Net.Sockets", throwOnError: true)!; - Type protocolTypeEnum = Type.GetType("System.Net.Sockets.ProtocolType, System.Net.Sockets", throwOnError: true)!; - - return new SocketReflection - { - SocketTypeDgram = Enum.Parse(socketTypeEnum, "Dgram"), - SocketTypeStream = Enum.Parse(socketTypeEnum, "Stream"), - ProtocolTypeUdp = Enum.Parse(protocolTypeEnum, "Udp"), - ProtocolTypeTcp = Enum.Parse(protocolTypeEnum, "Tcp"), - Constructor = socketType.GetConstructor(new[] { typeof(AddressFamily), socketTypeEnum, protocolTypeEnum })!, - ConnectAsyncMethod = socketType.GetMethod("ConnectAsync", new[] { typeof(EndPoint), typeof(CancellationToken) })!, - SendAsyncMethod = socketType.GetMethod("SendAsync", new[] { typeof(ReadOnlyMemory), typeof(CancellationToken) })!, - ReceiveAsyncMethod = socketType.GetMethod("ReceiveAsync", new[] { typeof(Memory), typeof(CancellationToken) })!, - ConnectMethod = socketType.GetMethod("Connect", new[] { typeof(EndPoint) })!, - SendMethod = socketType.GetMethod("Send", new[] { typeof(ReadOnlySpan) })!, - ReceiveMethod = socketType.GetMethod("Receive", new[] { typeof(Span) })!, - BeginConnectMethod = socketType.GetMethod("BeginConnect", new[] { typeof(EndPoint), typeof(AsyncCallback), typeof(object) })!, - EndConnectMethod = socketType.GetMethod("EndConnect", new[] { typeof(IAsyncResult) })!, - DisposeMethod = socketType.GetMethod("Dispose", Type.EmptyTypes)!, - SetSendTimeoutMethod = socketType.GetProperty("SendTimeout")!.GetSetMethod()!, - SetReceiveTimeoutMethod = socketType.GetProperty("ReceiveTimeout")!.GetSetMethod()!, - }; - } - + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, + "System.Net.Sockets.SafeSocketHandle", "System.Net.Sockets")] public DnsSocket(AddressFamily addressFamily, bool stream) { - SocketReflection reflection = s_reflection; - object socket; try { - socket = reflection.Constructor.Invoke(new object[] + _socket = s_socketConstructor.Invoke(new object[] { addressFamily, - stream ? reflection.SocketTypeStream : reflection.SocketTypeDgram, - stream ? reflection.ProtocolTypeTcp : reflection.ProtocolTypeUdp, + stream ? s_socketTypeStream : s_socketTypeDgram, + stream ? s_protocolTypeTcp : s_protocolTypeUdp, })!; } catch (TargetInvocationException e) when (e.InnerException is not null) { ExceptionDispatchInfo.Throw(e.InnerException); - throw; // Unreachable, satisfies definite-assignment. + throw; } + } - _connectAsync = reflection.ConnectAsyncMethod.CreateDelegate>(socket); - _sendAsync = reflection.SendAsyncMethod.CreateDelegate, CancellationToken, ValueTask>>(socket); - _receiveAsync = reflection.ReceiveAsyncMethod.CreateDelegate, CancellationToken, ValueTask>>(socket); - _connect = reflection.ConnectMethod.CreateDelegate>(socket); - _send = reflection.SendMethod.CreateDelegate(socket); - _receive = reflection.ReceiveMethod.CreateDelegate(socket); - _beginConnect = reflection.BeginConnectMethod.CreateDelegate>(socket); - _endConnect = reflection.EndConnectMethod.CreateDelegate>(socket); - _setSendTimeout = reflection.SetSendTimeoutMethod.CreateDelegate>(socket); - _setReceiveTimeout = reflection.SetReceiveTimeoutMethod.CreateDelegate>(socket); - _dispose = reflection.DisposeMethod.CreateDelegate(socket); + public DnsSocket(IntPtr fileDescriptor) + { + object safeHandle = CreateSafeSocketHandle(fileDescriptor, ownsHandle: false); + _socket = CreateSocket(safeHandle); } - public int SendTimeout { set => _setSendTimeout(value); } + private readonly object _socket; + + public int SendTimeout { set => SetSendTimeout(_socket, value); } - public int ReceiveTimeout { set => _setReceiveTimeout(value); } + public int ReceiveTimeout { set => SetReceiveTimeout(_socket, value); } public ValueTask ConnectAsync(EndPoint remoteEndPoint, CancellationToken cancellationToken) => - _connectAsync(remoteEndPoint, cancellationToken); + ConnectAsync(_socket, remoteEndPoint, cancellationToken); public ValueTask SendAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) => - _sendAsync(buffer, cancellationToken); + SendAsync(_socket, buffer, cancellationToken); public ValueTask ReceiveAsync(Memory buffer, CancellationToken cancellationToken) => - _receiveAsync(buffer, cancellationToken); + ReceiveAsync(_socket, buffer, cancellationToken); - public void Connect(EndPoint remoteEndPoint) => _connect(remoteEndPoint); + public ValueTask ReceiveAsync(Memory buffer, bool peek, CancellationToken cancellationToken) => + peek + ? ReceiveAsync(_socket, buffer, s_socketFlagsPeek, cancellationToken) + : ReceiveAsync(_socket, buffer, cancellationToken); - public int Send(ReadOnlySpan buffer) => _send(buffer); + public void Connect(EndPoint remoteEndPoint) => Connect(_socket, remoteEndPoint); - public int Receive(Span buffer) => _receive(buffer); + public int Send(ReadOnlySpan buffer) => Send(_socket, buffer); + + public int Receive(Span buffer) => Receive(_socket, buffer); // Connects synchronously with an explicit timeout so an unreachable TCP endpoint cannot // block indefinitely. Throws a timed-out SocketException when the timeout elapses. public void ConnectWithTimeout(EndPoint remoteEndPoint, TimeSpan timeout) { - IAsyncResult asyncResult = _beginConnect(remoteEndPoint, null, null); + IAsyncResult asyncResult = BeginConnect(_socket, remoteEndPoint, null, null); try { if (!asyncResult.AsyncWaitHandle.WaitOne(timeout)) @@ -154,7 +108,7 @@ public void ConnectWithTimeout(EndPoint remoteEndPoint, TimeSpan timeout) Dispose(); throw new SocketException((int)SocketError.TimedOut); } - _endConnect(asyncResult); + EndConnect(_socket, asyncResult); } finally { @@ -162,6 +116,49 @@ public void ConnectWithTimeout(EndPoint remoteEndPoint, TimeSpan timeout) } } - public void Dispose() => _dispose(); + public void Dispose() => ((IDisposable)_socket).Dispose(); + + [UnsafeAccessor(UnsafeAccessorKind.Constructor)] + [return: UnsafeAccessorType(SocketTypeName)] + private static extern object CreateSocket([UnsafeAccessorType(SafeSocketHandleTypeName)] object safeHandle); + + [UnsafeAccessor(UnsafeAccessorKind.Constructor)] + [return: UnsafeAccessorType(SafeSocketHandleTypeName)] + private static extern object CreateSafeSocketHandle(IntPtr handle, bool ownsHandle); + + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ConnectAsync")] + private static extern ValueTask ConnectAsync([UnsafeAccessorType(SocketTypeName)] object socket, EndPoint remoteEndPoint, CancellationToken cancellationToken); + + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "SendAsync")] + private static extern ValueTask SendAsync([UnsafeAccessorType(SocketTypeName)] object socket, ReadOnlyMemory buffer, CancellationToken cancellationToken); + + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ReceiveAsync")] + private static extern ValueTask ReceiveAsync([UnsafeAccessorType(SocketTypeName)] object socket, Memory buffer, CancellationToken cancellationToken); + + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ReceiveAsync")] + private static extern ValueTask ReceiveAsync([UnsafeAccessorType(SocketTypeName)] object socket, Memory buffer, + [UnsafeAccessorType(SocketFlagsTypeName)] object flags, CancellationToken cancellationToken); + + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "Connect")] + private static extern void Connect([UnsafeAccessorType(SocketTypeName)] object socket, EndPoint remoteEndPoint); + + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "Send")] + private static extern int Send([UnsafeAccessorType(SocketTypeName)] object socket, ReadOnlySpan buffer); + + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "Receive")] + private static extern int Receive([UnsafeAccessorType(SocketTypeName)] object socket, Span buffer); + + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "BeginConnect")] + private static extern IAsyncResult BeginConnect([UnsafeAccessorType(SocketTypeName)] object socket, EndPoint remoteEndPoint, + AsyncCallback? callback, object? state); + + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "EndConnect")] + private static extern void EndConnect([UnsafeAccessorType(SocketTypeName)] object socket, IAsyncResult asyncResult); + + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "set_SendTimeout")] + private static extern void SetSendTimeout([UnsafeAccessorType(SocketTypeName)] object socket, int value); + + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "set_ReceiveTimeout")] + private static extern void SetReceiveTimeout([UnsafeAccessorType(SocketTypeName)] object socket, int value); } } diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs index 0bddf7bd218334..788e71c474bda7 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverLoopbackTest.cs @@ -109,7 +109,7 @@ private static async Task> ResolveNs(bool async, DnsResolver // ---- Address resolution ---- - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] [InlineData(false)] [InlineData(true)] public async Task ResolveAddresses_Unspecified_ReturnsBothV4AndV6(bool async) @@ -126,7 +126,7 @@ public async Task ResolveAddresses_Unspecified_ReturnsBothV4AndV6(bool async) Assert.Contains(result.Records, a => a.Address.ToString() == "fd00::1"); } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] [InlineData(false)] [InlineData(true)] public async Task ResolveAddresses_IPv4Only_ReturnsOnlyV4(bool async) @@ -143,7 +143,7 @@ public async Task ResolveAddresses_IPv4Only_ReturnsOnlyV4(bool async) Assert.Equal("10.0.0.2", record.Address.ToString()); } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] [InlineData(false)] [InlineData(true)] public async Task ResolveAddresses_IPv6Only_ReturnsOnlyV6(bool async) @@ -159,7 +159,7 @@ public async Task ResolveAddresses_IPv6Only_ReturnsOnlyV6(bool async) Assert.Equal("fd00::1", record.Address.ToString()); } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] [InlineData(false)] [InlineData(true)] public async Task ResolveAddresses_AddressFamilyV4_QueriesOnlyA(bool async) @@ -174,7 +174,7 @@ public async Task ResolveAddresses_AddressFamilyV4_QueriesOnlyA(bool async) Assert.Equal("192.0.2.7", record.Address.ToString()); } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] [InlineData(false)] [InlineData(true)] public async Task ResolveAddresses_HasTtl(bool async) @@ -195,7 +195,7 @@ public async Task ResolveAddresses_HasTtl(bool async) // The following behaviors are specific to the managed resolver; the Windows PAL // delegates server failover and record validation to DnsQueryEx. - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] [InlineData(false)] [InlineData(true)] public async Task ResolveAddresses_ServerFailure_FailsOverToNextServer(bool async) @@ -219,7 +219,7 @@ public async Task ResolveAddresses_ServerFailure_FailsOverToNextServer(bool asyn Assert.True(failing.RequestCount > 0, "The first (failing) server should have been queried."); } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] [InlineData(false)] [InlineData(true)] public async Task ResolveAddresses_AllServersFail_ReturnsServerFailure(bool async) @@ -234,7 +234,7 @@ public async Task ResolveAddresses_AllServersFail_ReturnsServerFailure(bool asyn Assert.Empty(result.Records); } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] [InlineData(false)] [InlineData(true)] public async Task ResolveAddresses_MalformedARecord_Throws(bool async) @@ -248,7 +248,7 @@ public async Task ResolveAddresses_MalformedARecord_Throws(bool async) } #endif - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] [InlineData(false)] [InlineData(true)] public async Task ResolveAddresses_Nxdomain_ReturnsNxDomain(bool async) @@ -278,7 +278,7 @@ public async Task ResolveAddresses_Nxdomain_ReturnsNxDomain(bool async) #endif } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] [InlineData(false)] [InlineData(true)] public async Task ResolveAddresses_NoData_ReturnsNoErrorWithEmptyRecords(bool async) @@ -307,7 +307,7 @@ public async Task ResolveAddresses_NoData_ReturnsNoErrorWithEmptyRecords(bool as #endif } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] [InlineData(false)] [InlineData(true)] public async Task ResolveAddresses_NoData_And_Nxdomain_AreDistinguishable(bool async) @@ -341,7 +341,7 @@ public async Task ResolveAddresses_NoData_And_Nxdomain_AreDistinguishable(bool a // ---- SRV ---- - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] [InlineData(false)] [InlineData(true)] public async Task ResolveSrv_ReturnsRecords(bool async) @@ -366,7 +366,7 @@ public async Task ResolveSrv_ReturnsRecords(bool async) Assert.Equal((ushort)20, s2.Priority); } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] [InlineData(false)] [InlineData(true)] public async Task ResolveSrv_IncludesAdditionalAddresses(bool async) @@ -391,7 +391,7 @@ public async Task ResolveSrv_IncludesAdditionalAddresses(bool async) Assert.Equal(2, s2.Addresses.Count); } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] [InlineData(false)] [InlineData(true)] public async Task ResolveSrv_NoAdditionalAddresses(bool async) @@ -409,7 +409,7 @@ public async Task ResolveSrv_NoAdditionalAddresses(bool async) // ---- MX / TXT / CNAME / PTR / NS ---- - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] [InlineData(false)] [InlineData(true)] public async Task ResolveMx_ReturnsRecords(bool async) @@ -429,7 +429,7 @@ public async Task ResolveMx_ReturnsRecords(bool async) Assert.Single(result.Records, m => m.Exchange == "mail2.test" && m.Preference == 20); } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] [InlineData(false)] [InlineData(true)] public async Task ResolveTxt_ReturnsValues(bool async) @@ -447,7 +447,7 @@ public async Task ResolveTxt_ReturnsValues(bool async) Assert.Contains(result.Records, t => t.Values.Count == 2 && t.Values[0] == "part1" && t.Values[1] == "part2"); } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] [InlineData(false)] [InlineData(true)] public async Task ResolveCName_ReturnsCanonicalName(bool async) @@ -463,7 +463,7 @@ public async Task ResolveCName_ReturnsCanonicalName(bool async) Assert.Equal("canonical.test", record.CanonicalName); } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] [InlineData(false)] [InlineData(true)] public async Task ResolvePtr_ReturnsName(bool async) @@ -479,7 +479,7 @@ public async Task ResolvePtr_ReturnsName(bool async) Assert.Equal("host.test", record.Name); } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] [InlineData(false)] [InlineData(true)] public async Task ResolveNs_ReturnsRecords(bool async) @@ -523,7 +523,7 @@ public async Task CustomServer_DefaultPortZero_IsAccepted(bool async) // ---- Cancellation while a query is in flight ---- - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] public async Task ResolveAddresses_CancellationInFlight_Throws() { using SemaphoreSlim queryReceived = new(0, 1); @@ -554,7 +554,7 @@ public async Task ResolveAddresses_CancellationInFlight_Throws() // ---- Telemetry ---- - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] [InlineData(false)] [InlineData(true)] public async Task ResolveAddresses_RecordsDurationMetric_CoversQueryTime(bool async) diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs index f91183c4e5c8ae..9fb5a279aa5ddf 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/DnsResolverTest.cs @@ -21,13 +21,13 @@ public class DnsResolverTest private const string TestNsHost = "microsoft.com"; private const string NonExistentHost = "this-name-definitely-does-not-exist.dotnet-test.invalid"; + public static bool IsSupportedPlatform => + PlatformDetection.IsNotMobile && PlatformDetection.IsNotBrowser && PlatformDetection.IsNotWasi; + // DnsResolver has no implementation on Browser or WASI; every query throws // PlatformNotSupportedException there. public static bool IsDnsResolverUnsupported => PlatformDetection.IsBrowser || PlatformDetection.IsWasi; - // Android cannot report the system-configured DNS servers, so the parameterless - // constructor throws there. Tests that never send a query specify a server explicitly - // so that they remain platform independent. private static DnsResolver CreateResolver() => new DnsResolver(new DnsResolverOptions { Servers = { new IPEndPoint(IPAddress.Loopback, 53) } }); @@ -49,17 +49,15 @@ public void DnsResolver_Construct_DefaultOptions_DoesNotThrow() [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsAndroid))] public void DnsResolver_Construct_DefaultOptions_ThrowsPlatformNotSupported() { - // Android exposes no readable resolver configuration, so a resolver that would - // have to use the system-configured servers cannot be created. Assert.Throws(() => new DnsResolver()); - Assert.Throws(() => Dns.ResolveAddresses(TestHost)); } [ConditionalFact(nameof(IsDnsResolverUnsupported))] public async Task DnsResolver_UnsupportedPlatform_ThrowsPlatformNotSupported() { - Assert.Throws(() => Dns.ResolveAddresses(TestHost)); - await Assert.ThrowsAsync(() => Dns.ResolveAddressesAsync(TestHost)); + using DnsResolver r = CreateResolver(); + Assert.Throws(() => r.ResolveAddresses(TestHost)); + await Assert.ThrowsAsync(() => r.ResolveAddressesAsync(TestHost)); } [Fact] @@ -97,6 +95,19 @@ public async Task DnsResolver_EmptyName_Throws() Assert.Throws(() => r.ResolveAddresses(string.Empty)); } + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [InlineData("\0")] + [InlineData("\0host")] + [InlineData("host\0")] + [InlineData("ho\0st")] + [InlineData("microsoft.com\0.invalid")] + public async Task DnsResolver_NameContainsNull_ThrowsArgumentException(string name) + { + using DnsResolver r = new DnsResolver(); + await Assert.ThrowsAsync(() => r.ResolveAddressesAsync(name)); + Assert.Throws(() => r.ResolveAddresses(name)); + } + [Fact] public async Task DnsResolver_Disposed_Throws() { @@ -125,6 +136,9 @@ public async Task DnsResolver_DisposeAsync_ThrowsOnUse() 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); @@ -165,16 +179,9 @@ public static TheoryData SynchronouslyCompletingQueryNames() }; } - // ---- Network tests (require outbound DNS) ---- - // - // DnsResolver is implemented on Windows (DnsQueryEx) and on all Unix-like platforms - // (the managed stub resolver); it is unsupported on Browser and WASI. These tests use - // the system-configured DNS servers, which the managed resolver reads from - // /etc/resolv.conf. Android has no readable resolver configuration (its - // IPInterfaceProperties.DnsAddresses throws PlatformNotSupportedException for the same - // reason), so it is excluded until the servers can be obtained from the platform. + // ---- Cross-platform network tests (require outbound DNS) ---- - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotAndroid), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalFact(typeof(DnsResolverTest), nameof(IsSupportedPlatform))] public async Task DnsResolver_PreCanceledToken_ReturnsCanceled() { using DnsResolver r = new DnsResolver(); @@ -222,7 +229,7 @@ public async Task ResolveAddresses_SynchronouslyCompletingQuery_DoesNotHang(bool } } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotAndroid), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(DnsResolverTest), nameof(IsSupportedPlatform))] [InlineData(false)] [InlineData(true)] [OuterLoop] @@ -239,7 +246,7 @@ public async Task ResolveAddresses_KnownName_ReturnsRecords(bool async) } } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotAndroid), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(DnsResolverTest), nameof(IsSupportedPlatform))] [InlineData(false)] [InlineData(true)] [OuterLoop] @@ -248,26 +255,86 @@ 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); + Assert.NotEmpty(result.Records); foreach (AddressRecord rec in result.Records) { Assert.Equal(AddressFamily.InterNetwork, rec.Address.AddressFamily); } } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotAndroid), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(DnsResolverTest), nameof(IsSupportedPlatform))] + [InlineData(false)] + [InlineData(true)] + [OuterLoop] + public async Task ResolveAddresses_CNameChain_WaitsForAddressRecords(bool async) + { + using DnsResolver resolver = new(); + DnsResult result = + await ResolveAddresses(async, resolver, TestCNameHost, AddressFamily.InterNetwork); + + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + Assert.NotEmpty(result.Records); + Assert.All(result.Records, record => + Assert.Equal(AddressFamily.InterNetwork, record.Address.AddressFamily)); + } + + [ConditionalTheory(typeof(DnsResolverTest), nameof(IsSupportedPlatform))] [InlineData(false)] [InlineData(true)] [OuterLoop] - [ActiveIssue("https://github.com/dotnet/runtime/issues/131188", typeof(PlatformDetection), nameof(PlatformDetection.IsWindowsServer2025))] 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); + // mDNSResponder can surface a negative answer as either NoSuchName (NxDomain) or + // NoSuchRecord (mapped to NoError with no records); accept either on macOS. + if (PlatformDetection.IsOSX) + { + Assert.Contains(result.ResponseCode, new[] { DnsResponseCode.NoError, DnsResponseCode.NxDomain }); + } + else + { + Assert.Equal(DnsResponseCode.NxDomain, result.ResponseCode); + } + Assert.Empty(result.Records); + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsOSX))] + [InlineData(false)] + [InlineData(true)] + [OuterLoop] + public async Task ResolveAddresses_NonExistent_CompletesPromptly(bool async) + { + using DnsResolver resolver = new(); + string hostName = $"{Guid.NewGuid():N}.{NonExistentHost}"; + Task> query = async + ? resolver.ResolveAddressesAsync(hostName) + : Task.Run(() => resolver.ResolveAddresses(hostName)); + + DnsResult result = await query.WaitAsync(TimeSpan.FromSeconds(10)); + + Assert.Contains(result.ResponseCode, new[] { DnsResponseCode.NoError, DnsResponseCode.NxDomain }); Assert.Empty(result.Records); } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotAndroid), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(DnsResolverTest), nameof(IsSupportedPlatform))] + [InlineData(false)] + [InlineData(true)] + [OuterLoop] + public async Task ResolveSrv_KnownName_ReturnsRecords(bool async) + { + using DnsResolver r = new DnsResolver(); + DnsResult result = await ResolveSrv(async, r, TestSrv); + Assert.Equal(DnsResponseCode.NoError, result.ResponseCode); + Assert.NotEmpty(result.Records); + foreach (SrvRecord rec in result.Records) + { + Assert.False(string.IsNullOrEmpty(rec.Target)); + Assert.NotEqual((ushort)0, rec.Port); + } + } + + [ConditionalTheory(typeof(DnsResolverTest), nameof(IsSupportedPlatform))] [InlineData(false)] [InlineData(true)] [OuterLoop] @@ -283,7 +350,7 @@ public async Task ResolveMx_KnownName_ReturnsRecords(bool async) } } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotAndroid), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(DnsResolverTest), nameof(IsSupportedPlatform))] [InlineData(false)] [InlineData(true)] [OuterLoop] @@ -299,7 +366,7 @@ public async Task ResolveTxt_KnownName_ReturnsRecords(bool async) } } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotAndroid), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(DnsResolverTest), nameof(IsSupportedPlatform))] [InlineData(false)] [InlineData(true)] [OuterLoop] @@ -307,15 +374,16 @@ public async Task ResolveCName_KnownName_ReturnsRecord(bool async) { using DnsResolver r = new DnsResolver(); DnsResult result = await ResolveCName(async, r, TestCNameHost); + // A CNAME query can legitimately return NODATA (NoError with no records) if the + // name only has A/AAAA records, so only validate any records that are returned. 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) + foreach (CNameRecord rec in result.Records) { - Assert.False(string.IsNullOrEmpty(result.Records[0].CanonicalName)); + Assert.False(string.IsNullOrEmpty(rec.CanonicalName)); } } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotAndroid), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(DnsResolverTest), nameof(IsSupportedPlatform))] [InlineData(false)] [InlineData(true)] [OuterLoop] @@ -331,7 +399,7 @@ public async Task ResolveNs_KnownName_ReturnsRecords(bool async) } } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotAndroid), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(DnsResolverTest), nameof(IsSupportedPlatform))] [InlineData(false)] [InlineData(true)] [OuterLoop] @@ -344,7 +412,7 @@ public async Task ResolvePtr_ByIPAddress_ReturnsRecord(bool async) Assert.False(string.IsNullOrEmpty(result.Records[0].Name)); } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotAndroid), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(DnsResolverTest), nameof(IsSupportedPlatform))] [InlineData(false)] [InlineData(true)] [OuterLoop] @@ -355,7 +423,7 @@ public async Task Static_Dns_ResolveAddresses_Works(bool async) Assert.NotEmpty(result.Records); } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotAndroid), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] [InlineData(false)] [InlineData(true)] [OuterLoop] @@ -393,19 +461,6 @@ public void DnsResolver_CustomServer_NonStandardPort_ThrowsPlatformNotSupported( Assert.Throws(() => new DnsResolver(opts)); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindows), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] - public void DnsResolver_CustomServer_NonStandardPort_IsAccepted() - { - // The managed resolver talks to each server endpoint directly, so a non-default - // port is supported. - DnsResolverOptions opts = new DnsResolverOptions - { - Servers = { new IPEndPoint(IPAddress.Loopback, 5353) } - }; - using DnsResolver r = new DnsResolver(opts); - Assert.NotNull(r); - } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] public void DnsResolver_CustomServers_MixedAddressFamilies_ThrowsArgumentException() { @@ -422,26 +477,9 @@ public void DnsResolver_CustomServers_MixedAddressFamilies_ThrowsArgumentExcepti Assert.Throws(() => new DnsResolver(opts)); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindows), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] - public void DnsResolver_CustomServers_MixedAddressFamilies_IsAccepted() - { - // The managed resolver opens a socket matching each server's address family, so a - // mixed IPv4/IPv6 server list is supported. - DnsResolverOptions opts = new DnsResolverOptions - { - Servers = - { - new IPEndPoint(IPAddress.Loopback, 53), - new IPEndPoint(IPAddress.IPv6Loopback, 53), - } - }; - using DnsResolver r = new DnsResolver(opts); - Assert.NotNull(r); - } - // ---- Reverse-arpa name building (covers both IPv4 and IPv6 paths used by ResolvePtr(IPAddress)) ---- - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotAndroid), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNotWasi))] + [ConditionalTheory(typeof(DnsResolverTest), nameof(IsSupportedPlatform))] [InlineData(false)] [InlineData(true)] [OuterLoop] 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 577477373c2aa9..a68977a99cf952 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 @@ -1,6 +1,6 @@ - $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-unix;$(NetCoreAppCurrent)-browser;$(NetCoreAppCurrent)-wasi + $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-unix;$(NetCoreAppCurrent)-osx;$(NetCoreAppCurrent)-browser;$(NetCoreAppCurrent)-wasi true true true diff --git a/src/libraries/System.Net.NameResolution/tests/UnitTests/DnsSdRecordParsingTests.cs b/src/libraries/System.Net.NameResolution/tests/UnitTests/DnsSdRecordParsingTests.cs new file mode 100644 index 00000000000000..9261917c083363 --- /dev/null +++ b/src/libraries/System.Net.NameResolution/tests/UnitTests/DnsSdRecordParsingTests.cs @@ -0,0 +1,139 @@ +// 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 Xunit; + +namespace System.Net.NameResolution.Tests +{ + public class DnsSdRecordParsingTests + { + [Theory] + [InlineData("2001:db8::1", 0u, 0L)] + [InlineData("fe80::1", 42u, 42L)] + public void TryParseAddress_AppliesInterfaceIndexOnlyToLinkLocalIPv6(string addressString, uint interfaceIndex, long expectedScopeId) + { + IPAddress address = IPAddress.Parse(addressString); + DnsSdRecord record = new DnsSdRecord(28, address.GetAddressBytes(), ttl: 60, interfaceIndex); + + Assert.True(DnsSdRecordParsing.TryParseAddress(record, out AddressRecord parsed)); + Assert.Equal(expectedScopeId, parsed.Address.ScopeId); + Assert.Equal(AddressFamily.InterNetworkV6, parsed.Address.AddressFamily); + } + + [Fact] + public void TryParseAddress_IPv4_Parses() + { + DnsSdRecord record = new DnsSdRecord(1, new byte[] { 10, 0, 0, 7 }, ttl: 120, interfaceIndex: 0); + + Assert.True(DnsSdRecordParsing.TryParseAddress(record, out AddressRecord parsed)); + Assert.Equal("10.0.0.7", parsed.Address.ToString()); + Assert.Equal(TimeSpan.FromSeconds(120), parsed.Ttl); + } + + [Theory] + [InlineData(0)] + [InlineData(3)] + [InlineData(5)] + [InlineData(15)] + public void TryParseAddress_InvalidLength_ReturnsFalse(int length) + { + DnsSdRecord record = new DnsSdRecord(1, new byte[length], ttl: 60, interfaceIndex: 0); + + Assert.False(DnsSdRecordParsing.TryParseAddress(record, out _)); + } + + [Fact] + public void TryParseSrv_RootTarget_ReturnsDot() + { + // priority=0, weight=0, port=0, name= + DnsSdRecord record = new DnsSdRecord(33, new byte[] { 0, 0, 0, 0, 0, 0, 0 }, ttl: 60, interfaceIndex: 0); + + Assert.True(DnsSdRecordParsing.TryParseSrv(record, out SrvRecord parsed)); + Assert.Equal(".", parsed.Target); + } + + [Fact] + public void TryParseSrv_ParsesFields() + { + // priority=1, weight=2, port=443, name="a" + byte[] data = { 0, 1, 0, 2, 1, 0xBB, 1, (byte)'a', 0 }; + DnsSdRecord record = new DnsSdRecord(33, data, ttl: 60, interfaceIndex: 0); + + Assert.True(DnsSdRecordParsing.TryParseSrv(record, out SrvRecord parsed)); + Assert.Equal(1, parsed.Priority); + Assert.Equal(2, parsed.Weight); + Assert.Equal(443, parsed.Port); + Assert.Equal("a", parsed.Target); + } + + [Fact] + public void TryParseMx_RootExchange_ReturnsDot() + { + // preference=0, name= + DnsSdRecord record = new DnsSdRecord(15, new byte[] { 0, 0, 0 }, ttl: 60, interfaceIndex: 0); + + Assert.True(DnsSdRecordParsing.TryParseMx(record, out MxRecord parsed)); + Assert.Equal(".", parsed.Exchange); + Assert.Equal(0, parsed.Preference); + } + + [Fact] + public void TryParseTxt_ParsesMultipleStrings() + { + byte[] data = { 3, (byte)'a', (byte)'b', (byte)'c', 2, (byte)'x', (byte)'y' }; + DnsSdRecord record = new DnsSdRecord(16, data, ttl: 60, interfaceIndex: 0); + + Assert.True(DnsSdRecordParsing.TryParseTxt(record, out TxtRecord parsed)); + Assert.Equal(new[] { "abc", "xy" }, parsed.Values); + } + + [Fact] + public void TryParseTxt_LengthExceedsRemaining_ReturnsFalse() + { + byte[] data = { 5, (byte)'a' }; + DnsSdRecord record = new DnsSdRecord(16, data, ttl: 60, interfaceIndex: 0); + + Assert.False(DnsSdRecordParsing.TryParseTxt(record, out _)); + } + + [Fact] + public void TryParseCName_ParsesDottedName() + { + byte[] data = { 3, (byte)'w', (byte)'w', (byte)'w', 7, (byte)'e', (byte)'x', (byte)'a', (byte)'m', (byte)'p', (byte)'l', (byte)'e', 0 }; + DnsSdRecord record = new DnsSdRecord(5, data, ttl: 60, interfaceIndex: 0); + + Assert.True(DnsSdRecordParsing.TryParseCName(record, out CNameRecord parsed)); + Assert.Equal("www.example", parsed.CanonicalName); + } + + [Fact] + public void TryParsePtr_UnterminatedName_ReturnsFalse() + { + byte[] data = { 3, (byte)'w', (byte)'w', (byte)'w' }; + DnsSdRecord record = new DnsSdRecord(12, data, ttl: 60, interfaceIndex: 0); + + Assert.False(DnsSdRecordParsing.TryParsePtr(record, out _)); + } + + [Fact] + public void TryParseNs_RejectsCompressionPointer() + { + byte[] data = { 0xC0, 0x00 }; + DnsSdRecord record = new DnsSdRecord(2, data, ttl: 60, interfaceIndex: 0); + + Assert.False(DnsSdRecordParsing.TryParseNs(record, out _)); + } + + [Fact] + public void TryParseNs_RejectsOverlongLabel() + { + byte[] data = new byte[65]; + data[0] = 64; + + DnsSdRecord record = new DnsSdRecord(2, data, ttl: 60, interfaceIndex: 0); + + Assert.False(DnsSdRecordParsing.TryParseNs(record, out _)); + } + } +} diff --git a/src/libraries/System.Net.NameResolution/tests/UnitTests/System.Net.NameResolution.Unit.Tests.csproj b/src/libraries/System.Net.NameResolution/tests/UnitTests/System.Net.NameResolution.Unit.Tests.csproj index dd9f3a8b394583..1fa247ba7bdd30 100644 --- a/src/libraries/System.Net.NameResolution/tests/UnitTests/System.Net.NameResolution.Unit.Tests.csproj +++ b/src/libraries/System.Net.NameResolution/tests/UnitTests/System.Net.NameResolution.Unit.Tests.csproj @@ -21,8 +21,12 @@ Link="ProductionCode\System\Net\DnsMessageWriter.cs" /> + + @@ -32,5 +36,6 @@ +