diff --git a/.github/workflows/custom-build.yml b/.github/workflows/custom-build.yml
index 88b6d57..dfb2bd0 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 -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
@@ -118,7 +119,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
@@ -148,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.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.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..d7c7cbd 100644
--- a/ARSoft.Tools.Net/ARSoft.Tools.Net.csproj
+++ b/ARSoft.Tools.Net/ARSoft.Tools.Net.csproj
@@ -20,11 +20,25 @@
$(VersionPrefix)
AnyCPU;x64;ARM64
+
+
+
+ true
+
+ snupkg
+
+ true
+
-
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
@@ -59,30 +73,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
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