From 45a33aa464bbea059e4dc53005871e06ee433a53 Mon Sep 17 00:00:00 2001 From: Radu Andrei Solomon Date: Thu, 15 May 2025 15:46:10 -0400 Subject: [PATCH 1/9] fix: IPV6 detection code invocation --- ARSoft.Tools.Net.sln | 20 +- ARSoft.Tools.Net/Dns/DnsClientBase.cs | 922 +++++++++++++------------- 2 files changed, 490 insertions(+), 452 deletions(-) diff --git a/ARSoft.Tools.Net.sln b/ARSoft.Tools.Net.sln index 6c26235..93b873b 100644 --- a/ARSoft.Tools.Net.sln +++ b/ARSoft.Tools.Net.sln @@ -7,22 +7,26 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ARSoft.Tools.Net", "ARSoft. EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|x64 = Debug|x64 - Release|x64 = Release|x64 - Debug|ARM64 = Debug|ARM64 - Release|ARM64 = Release|ARM64 Debug|Any CPU = Debug|Any CPU + Debug|ARM64 = Debug|ARM64 + Debug|x64 = Debug|x64 Release|Any CPU = Release|Any CPU + Release|ARM64 = Release|ARM64 + Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {65BFA748-C640-49B0-B506-34BBB165233A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {65BFA748-C640-49B0-B506-34BBB165233A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {65BFA748-C640-49B0-B506-34BBB165233A}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {65BFA748-C640-49B0-B506-34BBB165233A}.Debug|ARM64.Build.0 = Debug|ARM64 {65BFA748-C640-49B0-B506-34BBB165233A}.Debug|x64.ActiveCfg = Debug|x64 {65BFA748-C640-49B0-B506-34BBB165233A}.Debug|x64.Build.0 = Debug|x64 - {65BFA748-C640-49B0-B506-34BBB165233A}.Release|x64.ActiveCfg = Release|x64 - {65BFA748-C640-49B0-B506-34BBB165233A}.Release|x64.Build.0 = Release|x64 - {65BFA748-C640-49B0-B506-34BBB165233A}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {65BFA748-C640-49B0-B506-34BBB165233A}.Debug|ARM64.Build.0 = Debug|ARM64 + {65BFA748-C640-49B0-B506-34BBB165233A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {65BFA748-C640-49B0-B506-34BBB165233A}.Release|Any CPU.Build.0 = Release|Any CPU {65BFA748-C640-49B0-B506-34BBB165233A}.Release|ARM64.ActiveCfg = Release|ARM64 {65BFA748-C640-49B0-B506-34BBB165233A}.Release|ARM64.Build.0 = Release|ARM64 + {65BFA748-C640-49B0-B506-34BBB165233A}.Release|x64.ActiveCfg = Release|x64 + {65BFA748-C640-49B0-B506-34BBB165233A}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ARSoft.Tools.Net/Dns/DnsClientBase.cs b/ARSoft.Tools.Net/Dns/DnsClientBase.cs index ead614a..1887e00 100644 --- a/ARSoft.Tools.Net/Dns/DnsClientBase.cs +++ b/ARSoft.Tools.Net/Dns/DnsClientBase.cs @@ -1,4 +1,5 @@ #region Copyright and License + // Copyright 2010..2024 Alexander Reinert // // This file is part of the ARSoft.Tools.Net - C# DNS client/server and SPF Library (https://github.com/alexreinert/ARSoft.Tools.Net) @@ -14,459 +15,492 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + #endregion -using System; +using Org.BouncyCastle.Crypto.Prng; +using Org.BouncyCastle.Security; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; -using Org.BouncyCastle.Crypto.Prng; -using Org.BouncyCastle.Security; using static ARSoft.Tools.Net.Dns.DnsServer; namespace ARSoft.Tools.Net.Dns { - public abstract class DnsClientBase : IDisposable - { - private class ReceivedMessage - { - public IPEndPoint ResponderAddress { get; } - public IPEndPoint LocalAddress { get; } - public TMessage Message { get; } - - public ReceivedMessage(IPEndPoint responderAddress, IPEndPoint localAddress, TMessage message) - { - ResponderAddress = responderAddress; - LocalAddress = localAddress; - Message = message; - } - } - - private static readonly SecureRandom _secureRandom = new(new CryptoApiRandomGenerator()); - - private readonly List _endpointInfos; - - private readonly IClientTransport[] _transports; - private readonly bool _disposeTransports; - - internal DnsClientBase(IEnumerable servers, int queryTimeout, IClientTransport[] transports, bool disposeTransports) - { - QueryTimeout = queryTimeout; - - _transports = transports; - _disposeTransports = disposeTransports; - - _endpointInfos = GetEndpointInfos(servers); - } - - /// - /// Milliseconds after which a query times out. - /// - public int QueryTimeout { get; } - - /// - /// Gets or set a value indicating whether the response is validated as described in - /// draft-vixie-dnsext-dns0x20-00 - /// - public bool IsResponseValidationEnabled { get; set; } - - /// - /// Gets or set a value indicating whether the query labels are used for additional validation as described in - /// draft-vixie-dnsext-dns0x20-00 - /// - // ReSharper disable once InconsistentNaming - public bool Is0x20ValidationEnabled { get; set; } - - protected TMessage? SendMessage(TMessage query) - where TMessage : DnsMessageBase, new() - { - return SendMessageAsync(query, CancellationToken.None).GetAwaiter().GetResult(); - } - - protected List SendMessageParallel(TMessage message) - where TMessage : DnsMessageBase, new() - { - return SendMessageParallelAsync(message, default).GetAwaiter().GetResult(); - } - - private bool ValidateResponse(TMessage message, TMessage response) - where TMessage : DnsMessageBase - { - if (IsResponseValidationEnabled) - { - message.ValidateResponse(response); - } - - return true; - } - - private DnsRawPackage PrepareMessage(TMessage message, out SelectTsigKey? tsigKeySelector, out byte[]? tsigOriginalMac) - where TMessage : DnsMessageBase, new() - { - if (message.TransactionID == 0) - { - message.TransactionID = (ushort) _secureRandom.Next(1, 0xffff); - } - - if (Is0x20ValidationEnabled) - { - message.Add0x20Bits(); - } - - var package = message.Encode(null, false, out tsigOriginalMac); - - if (message.TSigOptions != null) - { - tsigKeySelector = (_, _, _) => message.TSigOptions!.KeyData; - } - else - { - tsigKeySelector = null; - } - - return package; - } - - protected async Task SendMessageAsync(TMessage query, CancellationToken token) - where TMessage : DnsMessageBase, new() - { - var package = PrepareMessage(query, out var tsigKeySelector, out var tsigOriginalMac); - - TMessage? response = null; - - foreach (var connectionTask in GetConnectionTasks(package, query.IsReliableSendingRequested, token)) - { - IClientConnection? connection = null; - - try - { - connection = await connectionTask; - - if (connection == null) - continue; - - var receivedMessage = await SendMessageAsync(package, connection, tsigKeySelector, tsigOriginalMac, token); - - if ((receivedMessage != null) && ValidateResponse(query, receivedMessage.Message)) - { - connection.RestartIdleTimeout(receivedMessage.Message.GetEDnsKeepAliveTimeout()); - - if (receivedMessage.Message.ReturnCode == ReturnCode.ServerFailure) - { - response = receivedMessage.Message; - continue; - } - - if (!receivedMessage.Message.IsReliableResendingRequested) - return receivedMessage.Message; - - var resendTransport = _transports.FirstOrDefault(t => t.SupportsReliableTransfer && t.MaximumAllowedQuerySize <= package.Length && t != connection.Transport); - - if (resendTransport != null) - { - using (var resendConnection = await resendTransport.ConnectAsync(new DnsClientEndpointInfo(false, receivedMessage.ResponderAddress.Address, receivedMessage.LocalAddress.Address), QueryTimeout, token)) - { - if (resendConnection == null) - { - response = receivedMessage.Message; - } - else - { - var resendResponse = await SendMessageAsync(package, resendConnection, tsigKeySelector, tsigOriginalMac, token); - - if ((resendResponse != null) - && ValidateResponse(query, resendResponse.Message) - && ((resendResponse.Message.ReturnCode != ReturnCode.ServerFailure))) - { - resendConnection.RestartIdleTimeout(receivedMessage.Message.GetEDnsKeepAliveTimeout()); - return resendResponse.Message; - } - else - { - resendConnection.MarkFaulty(); - response = receivedMessage.Message; - } - } - } - } - } - else - { - connection.MarkFaulty(); - } - } - catch (Exception e) - { - Trace.TraceError("Error on dns query: " + e); - connection?.MarkFaulty(); - } - finally - { - connection?.Dispose(); - } - } - - return response; - } - - private IEnumerable> GetConnectionTasks(DnsRawPackage package, bool isReliableTransportRequested, CancellationToken token) - { - foreach (var transport in _transports) - { - if (transport.SupportsPooledConnections - && package.Length <= transport.MaximumAllowedQuerySize - && (!isReliableTransportRequested || transport.SupportsReliableTransfer)) - { - foreach (var endpointInfo in _endpointInfos) - { - yield return transport.GetPooledConnectionAsync(endpointInfo, token); - } - } - } - - foreach (var transport in _transports) - { - if (package.Length <= transport.MaximumAllowedQuerySize - && (!isReliableTransportRequested || transport.SupportsReliableTransfer)) - { - foreach (var endpointInfo in _endpointInfos) - { - yield return transport.ConnectAsync(endpointInfo, QueryTimeout, token); - } - } - } - } - - private async Task?> SendMessageAsync(DnsRawPackage package, IClientConnection connection, SelectTsigKey? tsigKeySelector, byte[]? tsigOriginalMac, CancellationToken token) - where TMessage : DnsMessageBase, new() - { - if (!await connection.SendAsync(package, token)) - return null; - - var resultData = await connection.ReceiveAsync(package.MessageIdentification, token); - - if (resultData == null) - return null; - - var response = DnsMessageBase.Parse(resultData.ToArraySegment(false), tsigKeySelector, tsigOriginalMac); - - var isNextMessageWaiting = response.IsNextMessageWaiting(false); - - while (isNextMessageWaiting) - { - resultData = await connection.ReceiveAsync(package.MessageIdentification, token); - - if (resultData == null) - return null; - - var nextResult = DnsMessageBase.Parse(resultData.ToArraySegment(false), tsigKeySelector, tsigOriginalMac); - - if (nextResult.ReturnCode == ReturnCode.ServerFailure) - return null; - - response.AddSubsequentResponse(nextResult); - isNextMessageWaiting = nextResult.IsNextMessageWaiting(true); - } - - return new ReceivedMessage(resultData.RemoteEndpoint, resultData.LocalEndpoint, response); - } - - protected async Task> SendMessageParallelAsync(TMessage message, CancellationToken token) - where TMessage : DnsMessageBase, new() - { - var package = PrepareMessage(message, out var tsigKeySelector, out var tsigOriginalMac); - - var multicastTransport = _transports.FirstOrDefault(t => t.SupportsMulticastTransfer); - - if (multicastTransport == null) - return new List(); - - if (package.Length > multicastTransport.MaximumAllowedQuerySize) - throw new ArgumentException("Message exceeds maximum size"); - - if (message.IsReliableSendingRequested) - throw new NotSupportedException("Sending reliable messages is not supported in multicast mode"); - - var results = new BlockingCollection(); - var cancellationTokenSource = new CancellationTokenSource(); - - cancellationTokenSource.CancelAfter(QueryTimeout); - - var tasks = _endpointInfos.Select(x => SendMessageParallelAsync(multicastTransport, x, message, package, tsigKeySelector, tsigOriginalMac, results, CancellationTokenSource.CreateLinkedTokenSource(token, cancellationTokenSource.Token).Token)).ToArray(); - - await Task.WhenAll(tasks); - - return results.ToList(); - } - - private async Task SendMessageParallelAsync(IClientTransport transport, DnsClientEndpointInfo endpointInfo, TMessage query, DnsRawPackage package, SelectTsigKey? tsigKeySelector, byte[]? tsigOriginalMac, BlockingCollection results, CancellationToken token) - where TMessage : DnsMessageBase, new() - { - using (var connection = await transport.ConnectAsync(endpointInfo, QueryTimeout, token)) - { - if (connection == null) - return; - - if (!await connection.SendAsync(package, token)) - return; - - while (true) - { - if (token.IsCancellationRequested) - break; - - var response = await connection.ReceiveAsync(package.MessageIdentification, token); - - if (response == null) - continue; - - TMessage result; - - try - { - result = DnsMessageBase.Parse(response.ToArraySegment(false), tsigKeySelector, tsigOriginalMac); - } - catch (Exception e) - { - Trace.TraceError("Error on dns query: " + e); - continue; - } - - if (!ValidateResponse(query, result)) - continue; - - if (result.ReturnCode == ReturnCode.ServerFailure) - continue; - - var resendTransport = _transports.FirstOrDefault(t => t.SupportsReliableTransfer && t.MaximumAllowedQuerySize <= package.Length && t != connection.Transport); - if (result.IsReliableResendingRequested && resendTransport != null) - { - ResendParallelMessageAsync(resendTransport, new DnsClientEndpointInfo(false, response.RemoteEndpoint.Address, response.LocalEndpoint.Address), query, package, tsigKeySelector, tsigOriginalMac, results, token).Start(); - } - else - { - results.Add(result, token); - } - } - } - } - - private async Task ResendParallelMessageAsync(IClientTransport transport, DnsClientEndpointInfo endpointInfo, TMessage query, DnsRawPackage package, SelectTsigKey? tsigKeySelector, byte[]? tsigOriginalMac, BlockingCollection results, CancellationToken token) - where TMessage : DnsMessageBase, new() - { - if (endpointInfo.IsMulticast && !transport.SupportsMulticastTransfer) - return; - - IClientConnection? connection = null; - - try - { - connection = await transport.ConnectAsync(endpointInfo, QueryTimeout, token); - - var response = await SendMessageAsync(package, connection!, tsigKeySelector, tsigOriginalMac, token); - - if ((response != null) - && ValidateResponse(query, response.Message)) - { - results.Add(response.Message, token); - } - else - { - connection?.MarkFaulty(); - } - } - catch (Exception e) - { - Trace.TraceError("Error on dns query: " + e); - connection?.MarkFaulty(); - } - finally - { - connection?.Dispose(); - } - } - - private List GetEndpointInfos(IEnumerable servers) - { - servers = servers.OrderBy(s => s.AddressFamily == AddressFamily.InterNetworkV6 ? 0 : 1).ToList(); - - List endpointInfos; - if (servers.Any(s => s.IsMulticast())) - { - var localIPs = NetworkInterface.GetAllNetworkInterfaces() - .Where(n => n.SupportsMulticast && (n.OperationalStatus == OperationalStatus.Up) && (n.NetworkInterfaceType != NetworkInterfaceType.Loopback)) - .SelectMany(n => n.GetIPProperties().UnicastAddresses.Select(a => a.Address)) - .Where(a => !IPAddress.IsLoopback(a) && ((a.AddressFamily == AddressFamily.InterNetwork) || a.IsIPv6LinkLocal)) - .ToList(); - - endpointInfos = servers - .SelectMany( - s => - { - if (s.IsMulticast()) - { - return localIPs - .Where(l => l.AddressFamily == s.AddressFamily) - .Select(l => new DnsClientEndpointInfo(true, s, l)); - } - else - { - return new[] - { - new DnsClientEndpointInfo(false, s, s.AddressFamily == AddressFamily.InterNetwork ? IPAddress.Any : IPAddress.IPv6Any) - }; - } - }).ToList(); - } - else - { - endpointInfos = servers - .Where(x => IsIPv6Enabled || (x.AddressFamily == AddressFamily.InterNetwork)) - .Select(s => new DnsClientEndpointInfo(false, s, s.AddressFamily == AddressFamily.InterNetwork ? IPAddress.Any : IPAddress.IPv6Any)) - .ToList(); - } - - return endpointInfos; - } - - private static bool IsIPv6Enabled { get; } = IsAnyIPv6Configured(); - - private static readonly IPAddress _ipvMappedNetworkAddress = IPAddress.Parse("0:0:0:0:0:FFFF::"); - - private static bool IsAnyIPv6Configured() - { - return NetworkInterface.GetAllNetworkInterfaces() - .Where(n => (n.OperationalStatus == OperationalStatus.Up) && (n.NetworkInterfaceType != NetworkInterfaceType.Loopback)) - .SelectMany(n => n.GetIPProperties().UnicastAddresses.Select(a => a.Address)) - .Any(a => !IPAddress.IsLoopback(a) && (a.AddressFamily == AddressFamily.InterNetworkV6) && !a.IsIPv6LinkLocal && !a.IsIPv6Teredo && !a.GetNetworkAddress(96).Equals(_ipvMappedNetworkAddress)); - } - - void IDisposable.Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool isDisposing) - { - if (_disposeTransports) - { - foreach (var transport in _transports) - { - transport.Dispose(); - } - } - } - - ~DnsClientBase() - { - Dispose(false); - } - } + public abstract class DnsClientBase : IDisposable + { + private class ReceivedMessage + { + public IPEndPoint ResponderAddress { get; } + public IPEndPoint LocalAddress { get; } + public TMessage Message { get; } + + public ReceivedMessage(IPEndPoint responderAddress, IPEndPoint localAddress, TMessage message) + { + ResponderAddress = responderAddress; + LocalAddress = localAddress; + Message = message; + } + } + + private static readonly SecureRandom _secureRandom = new(new CryptoApiRandomGenerator()); + + private readonly List _endpointInfos; + + private readonly IClientTransport[] _transports; + private readonly bool _disposeTransports; + + internal DnsClientBase(IEnumerable servers, int queryTimeout, IClientTransport[] transports, + bool disposeTransports) + { + QueryTimeout = queryTimeout; + + _transports = transports; + _disposeTransports = disposeTransports; + + _endpointInfos = GetEndpointInfos(servers); + } + + /// + /// Milliseconds after which a query times out. + /// + public int QueryTimeout { get; } + + /// + /// Gets or set a value indicating whether the response is validated as described in + /// draft-vixie-dnsext-dns0x20-00 + /// + public bool IsResponseValidationEnabled { get; set; } + + /// + /// Gets or set a value indicating whether the query labels are used for additional validation as described in + /// draft-vixie-dnsext-dns0x20-00 + /// + // ReSharper disable once InconsistentNaming + public bool Is0x20ValidationEnabled { get; set; } + + protected TMessage? SendMessage(TMessage query) + where TMessage : DnsMessageBase, new() + { + return SendMessageAsync(query, CancellationToken.None).GetAwaiter().GetResult(); + } + + protected List SendMessageParallel(TMessage message) + where TMessage : DnsMessageBase, new() + { + return SendMessageParallelAsync(message, default).GetAwaiter().GetResult(); + } + + private bool ValidateResponse(TMessage message, TMessage response) + where TMessage : DnsMessageBase + { + if (IsResponseValidationEnabled) + { + message.ValidateResponse(response); + } + + return true; + } + + private DnsRawPackage PrepareMessage(TMessage message, out SelectTsigKey? tsigKeySelector, + out byte[]? tsigOriginalMac) + where TMessage : DnsMessageBase, new() + { + if (message.TransactionID == 0) + { + message.TransactionID = (ushort)_secureRandom.Next(1, 0xffff); + } + + if (Is0x20ValidationEnabled) + { + message.Add0x20Bits(); + } + + var package = message.Encode(null, false, out tsigOriginalMac); + + if (message.TSigOptions != null) + { + tsigKeySelector = (_, _, _) => message.TSigOptions!.KeyData; + } + else + { + tsigKeySelector = null; + } + + return package; + } + + protected async Task SendMessageAsync(TMessage query, CancellationToken token) + where TMessage : DnsMessageBase, new() + { + var package = PrepareMessage(query, out var tsigKeySelector, out var tsigOriginalMac); + + TMessage? response = null; + + foreach (var connectionTask in GetConnectionTasks(package, query.IsReliableSendingRequested, token)) + { + IClientConnection? connection = null; + + try + { + connection = await connectionTask; + + if (connection == null) + continue; + + var receivedMessage = await SendMessageAsync(package, connection, tsigKeySelector, + tsigOriginalMac, token); + + if ((receivedMessage != null) && ValidateResponse(query, receivedMessage.Message)) + { + connection.RestartIdleTimeout(receivedMessage.Message.GetEDnsKeepAliveTimeout()); + + if (receivedMessage.Message.ReturnCode == ReturnCode.ServerFailure) + { + response = receivedMessage.Message; + continue; + } + + if (!receivedMessage.Message.IsReliableResendingRequested) + return receivedMessage.Message; + + var resendTransport = _transports.FirstOrDefault(t => + t.SupportsReliableTransfer && t.MaximumAllowedQuerySize <= package.Length && + t != connection.Transport); + + if (resendTransport != null) + { + using (var resendConnection = await resendTransport.ConnectAsync( + new DnsClientEndpointInfo(false, receivedMessage.ResponderAddress.Address, + receivedMessage.LocalAddress.Address), QueryTimeout, token)) + { + if (resendConnection == null) + { + response = receivedMessage.Message; + } + else + { + var resendResponse = await SendMessageAsync(package, resendConnection, + tsigKeySelector, tsigOriginalMac, token); + + if ((resendResponse != null) + && ValidateResponse(query, resendResponse.Message) + && ((resendResponse.Message.ReturnCode != ReturnCode.ServerFailure))) + { + resendConnection.RestartIdleTimeout(receivedMessage.Message + .GetEDnsKeepAliveTimeout()); + return resendResponse.Message; + } + else + { + resendConnection.MarkFaulty(); + response = receivedMessage.Message; + } + } + } + } + } + else + { + connection.MarkFaulty(); + } + } + catch (Exception e) + { + Trace.TraceError("Error on dns query: " + e); + connection?.MarkFaulty(); + } + finally + { + connection?.Dispose(); + } + } + + return response; + } + + private IEnumerable> GetConnectionTasks(DnsRawPackage package, + bool isReliableTransportRequested, CancellationToken token) + { + foreach (var transport in _transports) + { + if (transport.SupportsPooledConnections + && package.Length <= transport.MaximumAllowedQuerySize + && (!isReliableTransportRequested || transport.SupportsReliableTransfer)) + { + foreach (var endpointInfo in _endpointInfos) + { + yield return transport.GetPooledConnectionAsync(endpointInfo, token); + } + } + } + + foreach (var transport in _transports) + { + if (package.Length <= transport.MaximumAllowedQuerySize + && (!isReliableTransportRequested || transport.SupportsReliableTransfer)) + { + foreach (var endpointInfo in _endpointInfos) + { + yield return transport.ConnectAsync(endpointInfo, QueryTimeout, token); + } + } + } + } + + private async Task?> SendMessageAsync(DnsRawPackage package, + IClientConnection connection, SelectTsigKey? tsigKeySelector, byte[]? tsigOriginalMac, + CancellationToken token) + where TMessage : DnsMessageBase, new() + { + if (!await connection.SendAsync(package, token)) + return null; + + var resultData = await connection.ReceiveAsync(package.MessageIdentification, token); + + if (resultData == null) + return null; + + var response = + DnsMessageBase.Parse(resultData.ToArraySegment(false), tsigKeySelector, tsigOriginalMac); + + var isNextMessageWaiting = response.IsNextMessageWaiting(false); + + while (isNextMessageWaiting) + { + resultData = await connection.ReceiveAsync(package.MessageIdentification, token); + + if (resultData == null) + return null; + + var nextResult = DnsMessageBase.Parse(resultData.ToArraySegment(false), tsigKeySelector, + tsigOriginalMac); + + if (nextResult.ReturnCode == ReturnCode.ServerFailure) + return null; + + response.AddSubsequentResponse(nextResult); + isNextMessageWaiting = nextResult.IsNextMessageWaiting(true); + } + + return new ReceivedMessage(resultData.RemoteEndpoint, resultData.LocalEndpoint, response); + } + + protected async Task> SendMessageParallelAsync(TMessage message, + CancellationToken token) + where TMessage : DnsMessageBase, new() + { + var package = PrepareMessage(message, out var tsigKeySelector, out var tsigOriginalMac); + + var multicastTransport = _transports.FirstOrDefault(t => t.SupportsMulticastTransfer); + + if (multicastTransport == null) + return new List(); + + if (package.Length > multicastTransport.MaximumAllowedQuerySize) + throw new ArgumentException("Message exceeds maximum size"); + + if (message.IsReliableSendingRequested) + throw new NotSupportedException("Sending reliable messages is not supported in multicast mode"); + + var results = new BlockingCollection(); + var cancellationTokenSource = new CancellationTokenSource(); + + cancellationTokenSource.CancelAfter(QueryTimeout); + + var tasks = _endpointInfos.Select(x => SendMessageParallelAsync(multicastTransport, x, message, package, + tsigKeySelector, tsigOriginalMac, results, + CancellationTokenSource.CreateLinkedTokenSource(token, cancellationTokenSource.Token).Token)).ToArray(); + + await Task.WhenAll(tasks); + + return results.ToList(); + } + + private async Task SendMessageParallelAsync(IClientTransport transport, + DnsClientEndpointInfo endpointInfo, TMessage query, DnsRawPackage package, SelectTsigKey? tsigKeySelector, + byte[]? tsigOriginalMac, BlockingCollection results, CancellationToken token) + where TMessage : DnsMessageBase, new() + { + using (var connection = await transport.ConnectAsync(endpointInfo, QueryTimeout, token)) + { + if (connection == null) + return; + + if (!await connection.SendAsync(package, token)) + return; + + while (true) + { + if (token.IsCancellationRequested) + break; + + var response = await connection.ReceiveAsync(package.MessageIdentification, token); + + if (response == null) + continue; + + TMessage result; + + try + { + result = DnsMessageBase.Parse(response.ToArraySegment(false), tsigKeySelector, + tsigOriginalMac); + } + catch (Exception e) + { + Trace.TraceError("Error on dns query: " + e); + continue; + } + + if (!ValidateResponse(query, result)) + continue; + + if (result.ReturnCode == ReturnCode.ServerFailure) + continue; + + var resendTransport = _transports.FirstOrDefault(t => + t.SupportsReliableTransfer && t.MaximumAllowedQuerySize <= package.Length && + t != connection.Transport); + if (result.IsReliableResendingRequested && resendTransport != null) + { + ResendParallelMessageAsync(resendTransport, + new DnsClientEndpointInfo(false, response.RemoteEndpoint.Address, + response.LocalEndpoint.Address), query, package, tsigKeySelector, tsigOriginalMac, + results, token).Start(); + } + else + { + results.Add(result, token); + } + } + } + } + + private async Task ResendParallelMessageAsync(IClientTransport transport, + DnsClientEndpointInfo endpointInfo, TMessage query, DnsRawPackage package, SelectTsigKey? tsigKeySelector, + byte[]? tsigOriginalMac, BlockingCollection results, CancellationToken token) + where TMessage : DnsMessageBase, new() + { + if (endpointInfo.IsMulticast && !transport.SupportsMulticastTransfer) + return; + + IClientConnection? connection = null; + + try + { + connection = await transport.ConnectAsync(endpointInfo, QueryTimeout, token); + + var response = + await SendMessageAsync(package, connection!, tsigKeySelector, tsigOriginalMac, token); + + if ((response != null) + && ValidateResponse(query, response.Message)) + { + results.Add(response.Message, token); + } + else + { + connection?.MarkFaulty(); + } + } + catch (Exception e) + { + Trace.TraceError("Error on dns query: " + e); + connection?.MarkFaulty(); + } + finally + { + connection?.Dispose(); + } + } + + private List GetEndpointInfos(IEnumerable servers) + { + servers = servers.OrderBy(s => s.AddressFamily == AddressFamily.InterNetworkV6 ? 0 : 1).ToList(); + + List endpointInfos; + if (servers.Any(s => s.IsMulticast())) + { + var localIPs = NetworkInterface.GetAllNetworkInterfaces() + .Where(n => n.SupportsMulticast && (n.OperationalStatus == OperationalStatus.Up) && + (n.NetworkInterfaceType != NetworkInterfaceType.Loopback)) + .SelectMany(n => n.GetIPProperties().UnicastAddresses.Select(a => a.Address)) + .Where(a => !IPAddress.IsLoopback(a) && + ((a.AddressFamily == AddressFamily.InterNetwork) || a.IsIPv6LinkLocal)) + .ToList(); + + endpointInfos = servers + .SelectMany(s => + { + if (s.IsMulticast()) + { + return localIPs + .Where(l => l.AddressFamily == s.AddressFamily) + .Select(l => new DnsClientEndpointInfo(true, s, l)); + } + else + { + return new[] + { + new DnsClientEndpointInfo(false, s, + s.AddressFamily == AddressFamily.InterNetwork ? IPAddress.Any : IPAddress.IPv6Any) + }; + } + }).ToList(); + } + else + { + endpointInfos = servers + .Where(x => IsIPv6Enabled || (x.AddressFamily == AddressFamily.InterNetwork)) + .Select(s => new DnsClientEndpointInfo(false, s, + s.AddressFamily == AddressFamily.InterNetwork ? IPAddress.Any : IPAddress.IPv6Any)) + .ToList(); + } + + return endpointInfos; + } + + private static bool IsIPv6Enabled + { + get => IsAnyIPv6Configured(); + } + + private static readonly IPAddress _ipvMappedNetworkAddress = IPAddress.Parse("0:0:0:0:0:FFFF::"); + + private static bool IsAnyIPv6Configured() + { + return NetworkInterface.GetAllNetworkInterfaces() + .Where(n => (n.OperationalStatus == OperationalStatus.Up) && + (n.NetworkInterfaceType != NetworkInterfaceType.Loopback)) + .SelectMany(n => n.GetIPProperties().UnicastAddresses.Select(a => a.Address)) + .Any(a => !IPAddress.IsLoopback(a) && (a.AddressFamily == AddressFamily.InterNetworkV6) && + !a.IsIPv6LinkLocal && !a.IsIPv6Teredo && + !a.GetNetworkAddress(96).Equals(_ipvMappedNetworkAddress)); + } + + void IDisposable.Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool isDisposing) + { + if (_disposeTransports) + { + foreach (var transport in _transports) + { + transport.Dispose(); + } + } + } + + ~DnsClientBase() + { + Dispose(false); + } + } } \ No newline at end of file From 5f52da77cf4d90b0b149b5697333d79d60d06ad7 Mon Sep 17 00:00:00 2001 From: Radu Andrei Solomon Date: Mon, 26 May 2025 14:51:04 -0400 Subject: [PATCH 2/9] fix: add pdbs to debug build --- ARSoft.Tools.Net.sln.DotSettings | 5 +++-- ARSoft.Tools.Net/ARSoft.Tools.Net.csproj | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/ARSoft.Tools.Net.sln.DotSettings b/ARSoft.Tools.Net.sln.DotSettings index f23078f..8382a58 100644 --- a/ARSoft.Tools.Net.sln.DotSettings +++ b/ARSoft.Tools.Net.sln.DotSettings @@ -1,6 +1,6 @@  Copyright and License - Copyright 2010..$CURRENT_YEAR$ Alexander Reinert + Copyright 2010..${CurrentDate.Year} Alexander Reinert This file is part of the ARSoft.Tools.Net - C# DNS client/server and SPF Library (https://github.com/alexreinert/ARSoft.Tools.Net) @@ -15,4 +15,5 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - \ No newline at end of file + + True \ No newline at end of file diff --git a/ARSoft.Tools.Net/ARSoft.Tools.Net.csproj b/ARSoft.Tools.Net/ARSoft.Tools.Net.csproj index 4193108..66f7ed9 100644 --- a/ARSoft.Tools.Net/ARSoft.Tools.Net.csproj +++ b/ARSoft.Tools.Net/ARSoft.Tools.Net.csproj @@ -59,30 +59,45 @@ True True + true + full + false + DEBUG;TRACE;PARAMETRIZABLE True True + true + full + false + DEBUG;TRACE;PARAMETRIZABLE True True + true + full + false + DEBUG;TRACE;PARAMETRIZABLE True True + pdbonly True True + pdbonly True True + pdbonly \ No newline at end of file From a5a3f1ca5b27ed10b7b693b9efba0f05811668fa Mon Sep 17 00:00:00 2001 From: Radu Andrei Solomon Date: Mon, 26 May 2025 15:05:03 -0400 Subject: [PATCH 3/9] fix: include pdb in zip --- .github/workflows/custom-build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/custom-build.yml b/.github/workflows/custom-build.yml index 88b6d57..b871ae3 100644 --- a/.github/workflows/custom-build.yml +++ b/.github/workflows/custom-build.yml @@ -118,7 +118,9 @@ jobs: uses: actions/upload-artifact@v4 with: name: "[${{ env.sanitized_branch_name }}] ARSoft Tools v${{ steps.export-version.outputs.dll_version }} ${{ inputs.configuration }} ${{ inputs.platform }}" - path: ${{ env.dll-path }}/*.dll + path: | + ${{ env.dll-path }}/*.dll + ${{ env.dll-path }}/*.pdb if-no-files-found: warn # Wait for a random duration between 1 and 10 seconds to allow the release to be created From d86db9266c5fca5a80ea7a537cc8bf9ece516813 Mon Sep 17 00:00:00 2001 From: Radu Andrei Solomon Date: Wed, 11 Jun 2025 16:52:02 -0400 Subject: [PATCH 4/9] fix: build and publish as github nuget --- .github/workflows/custom-build.yml | 22 ++++++++++++++++++++-- ARSoft.Tools.Net/ARSoft.Tools.Net.csproj | 13 +++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/.github/workflows/custom-build.yml b/.github/workflows/custom-build.yml index b871ae3..70b95d4 100644 --- a/.github/workflows/custom-build.yml +++ b/.github/workflows/custom-build.yml @@ -90,7 +90,8 @@ jobs: # Build solution - name: Build solution - run: msbuild ${{ env.solution-name }} /p:Configuration="${{ inputs.configuration }}" /p:TargetFramework="${{ inputs.framework }}" /p:Platform="${{ inputs.platform }}" + # run: msbuild ${{ env.solution-name }} /p:Configuration="${{ inputs.configuration }}" /p:TargetFramework="${{ inputs.framework }}" /p:Platform="${{ inputs.platform }}" + run: dotnet pack ${{ env.solution-name }} --configuration "${{ inputs.configuration }}" --framework "${{ inputs.framework }}" --runtime "${{ inputs.platform }}" --output "${{ env.dll-path }}" # Set versioned DLL file path in environment - name: Set versioned DLL file path in environment @@ -150,9 +151,26 @@ jobs: echo "message=Build failed, check annotations for details" >> $GITHUB_OUTPUT shell: bash + upload_packages: + name: Publish package to NuGet + needs: [build, check_trigger] + runs-on: ubuntu-latest + container: mcr.microsoft.com/dotnet/sdk:latest + steps: + - uses: actions/download-artifact@v4 + name: Download Artifact + with: + name: build + path: ${{ env.dll-path }}/ + + - name: Upload Nuget Packages + run: | + dotnet nuget push "outputs/*.nupkg" --api-key ${{ secrets.GH_TOKEN }} --source https://nuget.pkg.github.com/${{ github.repository_owner }} + dotnet nuget push "outputs/*.snupkg" --api-key ${{ secrets.GH_TOKEN }} --source https://nuget.pkg.github.com/${{ github.repository_owner }} + check_status_and_report: name: Check action result - needs: [build, check_trigger] + needs: [build, upload_packages, check_trigger] if: needs.check_trigger.outputs.run_build == 'true' outputs: slack_message_body: ${{ steps.create_message.outputs.message_body }} diff --git a/ARSoft.Tools.Net/ARSoft.Tools.Net.csproj b/ARSoft.Tools.Net/ARSoft.Tools.Net.csproj index 66f7ed9..adaecad 100644 --- a/ARSoft.Tools.Net/ARSoft.Tools.Net.csproj +++ b/ARSoft.Tools.Net/ARSoft.Tools.Net.csproj @@ -20,11 +20,24 @@ $(VersionPrefix) AnyCPU;x64;ARM64 + + + + true + + snupkg + + true + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + From 62b9767f71a277947fef18216dfc9bc133c503cf Mon Sep 17 00:00:00 2001 From: Radu Andrei Solomon Date: Wed, 11 Jun 2025 17:11:58 -0400 Subject: [PATCH 5/9] fix: pipeline --- .github/workflows/custom-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/custom-build.yml b/.github/workflows/custom-build.yml index 70b95d4..1728f38 100644 --- a/.github/workflows/custom-build.yml +++ b/.github/workflows/custom-build.yml @@ -91,7 +91,7 @@ jobs: # Build solution - name: Build solution # run: msbuild ${{ env.solution-name }} /p:Configuration="${{ inputs.configuration }}" /p:TargetFramework="${{ inputs.framework }}" /p:Platform="${{ inputs.platform }}" - run: dotnet pack ${{ env.solution-name }} --configuration "${{ inputs.configuration }}" --framework "${{ inputs.framework }}" --runtime "${{ inputs.platform }}" --output "${{ env.dll-path }}" + run: dotnet pack ${{ env.solution-name }} --configuration "${{ inputs.configuration }}" -p:TargetFrameworks="${{ inputs.framework }}" --runtime "${{ inputs.platform }}" --output "${{ env.dll-path }}" # Set versioned DLL file path in environment - name: Set versioned DLL file path in environment From fa3cec10b8e8fcb31d283215da8368e160d26f1b Mon Sep 17 00:00:00 2001 From: Radu Andrei Solomon Date: Wed, 11 Jun 2025 17:16:58 -0400 Subject: [PATCH 6/9] fix: pipline --- .github/workflows/custom-build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/custom-build.yml b/.github/workflows/custom-build.yml index 1728f38..dfb592e 100644 --- a/.github/workflows/custom-build.yml +++ b/.github/workflows/custom-build.yml @@ -14,9 +14,9 @@ on: type: choice description: Platform to build for options: - - x64 - - ARM64 - default: x64 + - win-x64 + - win-ARM64 + default: win-x64 framework: type: choice description: Target net framework From 5bbcca0f721132601504542a3b65204632c3a254 Mon Sep 17 00:00:00 2001 From: Radu Andrei Solomon Date: Wed, 11 Jun 2025 17:43:37 -0400 Subject: [PATCH 7/9] fix: pipeline --- .github/workflows/custom-build.yml | 4 ++-- ARSoft.Tools.Net/ARSoft.Tools.Net.csproj | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/custom-build.yml b/.github/workflows/custom-build.yml index dfb592e..76e71a4 100644 --- a/.github/workflows/custom-build.yml +++ b/.github/workflows/custom-build.yml @@ -90,8 +90,8 @@ jobs: # Build solution - name: Build solution - # run: msbuild ${{ env.solution-name }} /p:Configuration="${{ inputs.configuration }}" /p:TargetFramework="${{ inputs.framework }}" /p:Platform="${{ inputs.platform }}" - run: dotnet pack ${{ env.solution-name }} --configuration "${{ inputs.configuration }}" -p:TargetFrameworks="${{ inputs.framework }}" --runtime "${{ inputs.platform }}" --output "${{ env.dll-path }}" + run: msbuild -t:pack ${{ env.solution-name }} /p:Configuration="${{ inputs.configuration }}" /p:TargetFramework="${{ inputs.framework }}" /p:Platform="${{ inputs.platform }}" + # run: dotnet pack ${{ env.solution-name }} --configuration "${{ inputs.configuration }}" -p:TargetFrameworks="${{ inputs.framework }}" --runtime "${{ inputs.platform }}" --output "${{ env.dll-path }}" # Set versioned DLL file path in environment - name: Set versioned DLL file path in environment diff --git a/ARSoft.Tools.Net/ARSoft.Tools.Net.csproj b/ARSoft.Tools.Net/ARSoft.Tools.Net.csproj index adaecad..4003650 100644 --- a/ARSoft.Tools.Net/ARSoft.Tools.Net.csproj +++ b/ARSoft.Tools.Net/ARSoft.Tools.Net.csproj @@ -34,6 +34,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive From 2831706fd253cf1454546b85f66ebeff3c8e80b3 Mon Sep 17 00:00:00 2001 From: Radu Andrei Solomon Date: Wed, 11 Jun 2025 17:45:16 -0400 Subject: [PATCH 8/9] fix: pipeline --- .github/workflows/custom-build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/custom-build.yml b/.github/workflows/custom-build.yml index 76e71a4..dfb2bd0 100644 --- a/.github/workflows/custom-build.yml +++ b/.github/workflows/custom-build.yml @@ -14,9 +14,9 @@ on: type: choice description: Platform to build for options: - - win-x64 - - win-ARM64 - default: win-x64 + - x64 + - ARM64 + default: x64 framework: type: choice description: Target net framework From e942cb8337b6e7df5fd9a3089c535a5b967854df Mon Sep 17 00:00:00 2001 From: Radu Andrei Solomon Date: Thu, 12 Jun 2025 10:50:36 -0400 Subject: [PATCH 9/9] fix: project dependecies --- ARSoft.Tools.Net/ARSoft.Tools.Net.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ARSoft.Tools.Net/ARSoft.Tools.Net.csproj b/ARSoft.Tools.Net/ARSoft.Tools.Net.csproj index 4003650..d7c7cbd 100644 --- a/ARSoft.Tools.Net/ARSoft.Tools.Net.csproj +++ b/ARSoft.Tools.Net/ARSoft.Tools.Net.csproj @@ -32,9 +32,9 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive