diff --git a/DnsClientX.Tests/QueryDnsByEndpoint.cs b/DnsClientX.Tests/QueryDnsByEndpoint.cs
index 4f931b86..11b1cfc2 100644
--- a/DnsClientX.Tests/QueryDnsByEndpoint.cs
+++ b/DnsClientX.Tests/QueryDnsByEndpoint.cs
@@ -18,6 +18,7 @@ public class QueryDnsByEndpoint {
#endif
[Theory]
public async Task ShouldWorkForTXT(DnsEndpoint endpoint) {
+ if (TestSkipHelpers.ShouldSkipEndpoint(endpoint)) return;
var response = await ClientX.QueryDns("github.com", DnsRecordType.TXT, endpoint);
foreach (DnsAnswer answer in response.Answers) {
Assert.True(answer.Name == "github.com");
@@ -48,6 +49,7 @@ public async Task ShouldWorkForTXT(DnsEndpoint endpoint) {
[InlineData(DnsEndpoint.GoogleQuic)]
#endif
public async Task ShouldWorkForA(DnsEndpoint endpoint) {
+ if (TestSkipHelpers.ShouldSkipEndpoint(endpoint)) return;
var response = await ClientX.QueryDns("evotec.pl", DnsRecordType.A, endpoint);
foreach (DnsAnswer answer in response.Answers) {
Assert.True(answer.Name == "evotec.pl");
@@ -78,6 +80,7 @@ public async Task ShouldWorkForA(DnsEndpoint endpoint) {
[InlineData(DnsEndpoint.GoogleQuic)]
#endif
public async Task ShouldWorkForPTR(DnsEndpoint endpoint) {
+ if (TestSkipHelpers.ShouldSkipEndpoint(endpoint)) return;
var response = await ClientX.QueryDns("1.1.1.1", DnsRecordType.PTR, endpoint);
foreach (DnsAnswer answer in response.Answers) {
Assert.True(answer.Data == "one.one.one.one");
diff --git a/DnsClientX.Tests/RegexCultureInvariantTests.cs b/DnsClientX.Tests/RegexCultureInvariantTests.cs
index 908f97b6..6565329d 100644
--- a/DnsClientX.Tests/RegexCultureInvariantTests.cs
+++ b/DnsClientX.Tests/RegexCultureInvariantTests.cs
@@ -1,4 +1,5 @@
using System.Globalization;
+using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading;
@@ -40,7 +41,8 @@ public void ConvertData_TlsaRecord_ConsistentAcrossCultures(string culture) {
[InlineData("tr-TR")]
public void FilterAnswersRegex_ConsistentAcrossCultures(string culture) {
var client = new ClientX();
- MethodInfo method = typeof(ClientX).GetMethod("FilterAnswersRegex", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ MethodInfo method = typeof(ClientX).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
+ .Single(m => m.Name == "FilterAnswersRegex" && m.GetParameters().Length == 3);
var answers = new[] {
new DnsAnswer {
Name = "example.com",
@@ -69,7 +71,8 @@ public void FilterAnswersRegex_ConsistentAcrossCultures(string culture) {
[InlineData("tr-TR")]
public void FilterAnswers_ConsistentAcrossCultures(string culture) {
var client = new ClientX();
- MethodInfo method = typeof(ClientX).GetMethod("FilterAnswers", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ MethodInfo method = typeof(ClientX).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
+ .Single(m => m.Name == "FilterAnswers" && m.GetParameters().Length == 3);
var answers = new[] {
new DnsAnswer {
Name = "example.com",
diff --git a/DnsClientX.Tests/ResolveFilterAliasTests.cs b/DnsClientX.Tests/ResolveFilterAliasTests.cs
new file mode 100644
index 00000000..63c8ff6d
--- /dev/null
+++ b/DnsClientX.Tests/ResolveFilterAliasTests.cs
@@ -0,0 +1,118 @@
+using System.Threading.Tasks;
+using Xunit;
+
+namespace DnsClientX.Tests {
+ ///
+ /// Tests alias handling for ResolveFilter APIs.
+ ///
+ public class ResolveFilterAliasTests {
+ private static DnsResponse CreateResponse(string name) {
+ return new DnsResponse {
+ Answers = new[] {
+ new DnsAnswer { Name = name, Type = DnsRecordType.CNAME, TTL = 60, DataRaw = "alias.example.com" },
+ new DnsAnswer { Name = name, Type = DnsRecordType.TXT, TTL = 120, DataRaw = "v=spf1 include:example.com -all\nother=record" }
+ }
+ };
+ }
+
+ ///
+ /// Ensures alias answers are kept alongside matching TXT lines when enabled.
+ ///
+ [Fact]
+ public async Task ResolveFilter_IncludeAliases_KeepsCnameAndMatchingTxt() {
+ using var client = new ClientX(DnsEndpoint.Cloudflare);
+ client.ResolverOverride = (name, type, ct) => Task.FromResult(CreateResponse(name));
+
+ var options = new ResolveFilterOptions(true);
+ var response = await client.ResolveFilter("example.com", DnsRecordType.TXT, "v=spf1", options, retryOnTransient: false);
+
+ Assert.NotNull(response.Answers);
+ Assert.Equal(2, response.Answers.Length);
+ Assert.Contains(response.Answers, answer => answer.Type == DnsRecordType.CNAME);
+ Assert.Contains(response.Answers, answer => answer.Type == DnsRecordType.TXT && answer.Data == "v=spf1 include:example.com -all");
+ }
+
+ ///
+ /// Ensures alias answers are not kept when alias inclusion is disabled.
+ ///
+ [Fact]
+ public async Task ResolveFilter_ExcludeAliases_DropsCname() {
+ using var client = new ClientX(DnsEndpoint.Cloudflare);
+ client.ResolverOverride = (name, type, ct) => Task.FromResult(CreateResponse(name));
+
+ var options = new ResolveFilterOptions(false);
+ var response = await client.ResolveFilter("example.com", DnsRecordType.TXT, "v=spf1", options, retryOnTransient: false);
+
+ Assert.Single(response.Answers);
+ Assert.Equal(DnsRecordType.TXT, response.Answers[0].Type);
+ }
+
+ ///
+ /// Ensures array-based ResolveFilter returns responses when only aliases match.
+ ///
+ [Fact]
+ public async Task ResolveFilter_Array_IncludeAliases_ReturnsResponseWithOnlyCname() {
+ using var client = new ClientX(DnsEndpoint.Cloudflare);
+ client.ResolverOverride = (name, type, ct) => Task.FromResult(CreateResponse(name));
+
+ var options = new ResolveFilterOptions(true);
+ var responses = await client.ResolveFilter(new[] { "example.com" }, DnsRecordType.TXT, "nomatch", options, retryOnTransient: false);
+
+ Assert.Single(responses);
+ Assert.Single(responses[0].Answers);
+ Assert.Equal(DnsRecordType.CNAME, responses[0].Answers[0].Type);
+ }
+
+ ///
+ /// Ensures empty filters still keep alias and requested type answers when enabled.
+ ///
+ [Fact]
+ public async Task ResolveFilter_IncludeAliases_EmptyFilter_ReturnsAliasAndType() {
+ using var client = new ClientX(DnsEndpoint.Cloudflare);
+ client.ResolverOverride = (name, type, ct) => Task.FromResult(CreateResponse(name));
+
+ var options = new ResolveFilterOptions(true);
+ var response = await client.ResolveFilter("example.com", DnsRecordType.TXT, string.Empty, options, retryOnTransient: false);
+
+ Assert.NotNull(response.Answers);
+ Assert.Equal(2, response.Answers.Length);
+ Assert.Contains(response.Answers, answer => answer.Type == DnsRecordType.CNAME);
+ Assert.Contains(response.Answers, answer => answer.Type == DnsRecordType.TXT);
+ }
+
+ ///
+ /// Ensures null filters are treated as empty when alias inclusion is enabled.
+ ///
+ [Fact]
+ public async Task ResolveFilter_IncludeAliases_NullFilter_TreatedAsEmpty() {
+ using var client = new ClientX(DnsEndpoint.Cloudflare);
+ client.ResolverOverride = (name, type, ct) => Task.FromResult(CreateResponse(name));
+
+ var options = new ResolveFilterOptions(true);
+ string? filter = null;
+#pragma warning disable CS8604 // Null is allowed for robustness testing.
+ var response = await client.ResolveFilter("example.com", DnsRecordType.TXT, filter, options, retryOnTransient: false);
+#pragma warning restore CS8604
+
+ Assert.NotNull(response.Answers);
+ Assert.Equal(2, response.Answers.Length);
+ Assert.Contains(response.Answers, answer => answer.Type == DnsRecordType.CNAME);
+ Assert.Contains(response.Answers, answer => answer.Type == DnsRecordType.TXT);
+ }
+
+ ///
+ /// Ensures alias filtering still respects the filter when querying alias types.
+ ///
+ [Fact]
+ public async Task ResolveFilter_IncludeAliases_AliasType_RespectsFilter() {
+ using var client = new ClientX(DnsEndpoint.Cloudflare);
+ client.ResolverOverride = (name, type, ct) => Task.FromResult(CreateResponse(name));
+
+ var options = new ResolveFilterOptions(true);
+ var response = await client.ResolveFilter("example.com", DnsRecordType.CNAME, "nomatch", options, retryOnTransient: false);
+
+ Assert.NotNull(response.Answers);
+ Assert.Empty(response.Answers);
+ }
+ }
+}
diff --git a/DnsClientX.Tests/ResolveFilterLineTests.cs b/DnsClientX.Tests/ResolveFilterLineTests.cs
index 043fc581..c5035d4b 100644
--- a/DnsClientX.Tests/ResolveFilterLineTests.cs
+++ b/DnsClientX.Tests/ResolveFilterLineTests.cs
@@ -1,3 +1,4 @@
+using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Xunit;
@@ -15,7 +16,8 @@ public class ResolveFilterLineTests {
[Fact]
public void FilterAnswers_ReturnsMatchingLine() {
var client = new ClientX();
- var method = typeof(ClientX).GetMethod("FilterAnswers", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ var method = typeof(ClientX).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
+ .Single(m => m.Name == "FilterAnswers" && m.GetParameters().Length == 3);
var answers = new[] { CreateTxt("line1\nline2") };
var result = (DnsAnswer[])method.Invoke(client, new object[] { answers, "line2", DnsRecordType.TXT })!;
Assert.Single(result);
@@ -28,7 +30,8 @@ public void FilterAnswers_ReturnsMatchingLine() {
[Fact]
public void FilterAnswersRegex_ReturnsMatchingLine() {
var client = new ClientX();
- var method = typeof(ClientX).GetMethod("FilterAnswersRegex", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ var method = typeof(ClientX).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
+ .Single(m => m.Name == "FilterAnswersRegex" && m.GetParameters().Length == 3);
var answers = new[] { CreateTxt("line1\nline2") };
var result = (DnsAnswer[])method.Invoke(client, new object[] { answers, new Regex("line2$"), DnsRecordType.TXT })!;
Assert.Single(result);
diff --git a/DnsClientX.Tests/ResolveFilterNullDataTests.cs b/DnsClientX.Tests/ResolveFilterNullDataTests.cs
index 2c1579f8..173ace5b 100644
--- a/DnsClientX.Tests/ResolveFilterNullDataTests.cs
+++ b/DnsClientX.Tests/ResolveFilterNullDataTests.cs
@@ -1,3 +1,4 @@
+using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Xunit;
@@ -22,7 +23,8 @@ private static DnsAnswer CreateAnswer(string dataRaw, DnsRecordType type) {
[Fact]
public void FilterAnswers_ShouldIgnoreEmptyData() {
var client = new ClientX();
- MethodInfo method = typeof(ClientX).GetMethod("FilterAnswers", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ MethodInfo method = typeof(ClientX).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
+ .Single(m => m.Name == "FilterAnswers" && m.GetParameters().Length == 3);
var answers = new[] { CreateAnswer(string.Empty, DnsRecordType.A) };
var result = (DnsAnswer[])method.Invoke(client, new object[] { answers, "test", DnsRecordType.A })!;
Assert.Empty(result);
@@ -34,7 +36,8 @@ public void FilterAnswers_ShouldIgnoreEmptyData() {
[Fact]
public void FilterAnswersRegex_ShouldIgnoreEmptyData() {
var client = new ClientX();
- MethodInfo method = typeof(ClientX).GetMethod("FilterAnswersRegex", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ MethodInfo method = typeof(ClientX).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
+ .Single(m => m.Name == "FilterAnswersRegex" && m.GetParameters().Length == 3);
var answers = new[] { CreateAnswer(string.Empty, DnsRecordType.A) };
var result = (DnsAnswer[])method.Invoke(client, new object[] { answers, new Regex("test", RegexOptions.CultureInvariant), DnsRecordType.A })!;
Assert.Empty(result);
@@ -46,7 +49,8 @@ public void FilterAnswersRegex_ShouldIgnoreEmptyData() {
[Fact]
public void HasMatchingAnswers_ShouldReturnFalseForEmptyData() {
var client = new ClientX();
- MethodInfo method = typeof(ClientX).GetMethod("HasMatchingAnswers", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ MethodInfo method = typeof(ClientX).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
+ .Single(m => m.Name == "HasMatchingAnswers" && m.GetParameters().Length == 3);
var answers = new[] { CreateAnswer(string.Empty, DnsRecordType.A) };
var result = (bool)method.Invoke(client, new object[] { answers, "test", DnsRecordType.A })!;
Assert.False(result);
@@ -58,7 +62,8 @@ public void HasMatchingAnswers_ShouldReturnFalseForEmptyData() {
[Fact]
public void HasMatchingAnswersRegex_ShouldReturnFalseForEmptyData() {
var client = new ClientX();
- MethodInfo method = typeof(ClientX).GetMethod("HasMatchingAnswersRegex", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ MethodInfo method = typeof(ClientX).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
+ .Single(m => m.Name == "HasMatchingAnswersRegex" && m.GetParameters().Length == 3);
var answers = new[] { CreateAnswer(string.Empty, DnsRecordType.A) };
var result = (bool)method.Invoke(client, new object[] { answers, new Regex("test", RegexOptions.CultureInvariant), DnsRecordType.A })!;
Assert.False(result);
diff --git a/DnsClientX.Tests/ResolveFirst.cs b/DnsClientX.Tests/ResolveFirst.cs
index 587d08e5..9e3c8d6f 100644
--- a/DnsClientX.Tests/ResolveFirst.cs
+++ b/DnsClientX.Tests/ResolveFirst.cs
@@ -24,6 +24,7 @@ public class ResolveFirst {
[InlineData(DnsEndpoint.OpenDNS)]
[InlineData(DnsEndpoint.OpenDNSFamily)]
public async Task ShouldWorkForTXT(DnsEndpoint endpoint) {
+ if (TestSkipHelpers.ShouldSkipEndpoint(endpoint)) return;
using var Client = new ClientX(endpoint);
var answer = await Client.ResolveFirst("github.com", DnsRecordType.TXT, cancellationToken: CancellationToken.None);
Assert.True(answer != null);
@@ -53,6 +54,7 @@ public async Task ShouldWorkForTXT(DnsEndpoint endpoint) {
[InlineData(DnsEndpoint.OpenDNS)]
[InlineData(DnsEndpoint.OpenDNSFamily)]
public async Task ShouldWorkForA(DnsEndpoint endpoint) {
+ if (TestSkipHelpers.ShouldSkipEndpoint(endpoint)) return;
using var Client = new ClientX(endpoint);
var answer = await Client.ResolveFirst("evotec.pl", DnsRecordType.A, cancellationToken: CancellationToken.None);
Assert.True(answer != null);
@@ -78,6 +80,7 @@ public async Task ShouldWorkForA(DnsEndpoint endpoint) {
[InlineData(DnsEndpoint.OpenDNS)]
[InlineData(DnsEndpoint.OpenDNSFamily)]
public void ShouldWorkForTXT_Sync(DnsEndpoint endpoint) {
+ if (TestSkipHelpers.ShouldSkipEndpoint(endpoint)) return;
using var Client = new ClientX(endpoint);
var answer = Client.ResolveFirstSync("github.com", DnsRecordType.TXT, cancellationToken: CancellationToken.None);
Assert.True(answer != null);
@@ -104,6 +107,7 @@ public void ShouldWorkForTXT_Sync(DnsEndpoint endpoint) {
[InlineData(DnsEndpoint.OpenDNS)]
[InlineData(DnsEndpoint.OpenDNSFamily)]
public void ShouldWorkForA_Sync(DnsEndpoint endpoint) {
+ if (TestSkipHelpers.ShouldSkipEndpoint(endpoint)) return;
using var Client = new ClientX(endpoint);
var answer = Client.ResolveFirstSync("evotec.pl", DnsRecordType.A, cancellationToken: CancellationToken.None);
Assert.True(answer != null);
diff --git a/DnsClientX.Tests/ResolveSync.cs b/DnsClientX.Tests/ResolveSync.cs
index a2bd6177..b8a3b137 100644
--- a/DnsClientX.Tests/ResolveSync.cs
+++ b/DnsClientX.Tests/ResolveSync.cs
@@ -24,6 +24,11 @@ private void LogDiagnostics(string message)
_output.WriteLine($"[Diagnostic] {message}");
}
+ private bool ShouldSkipEndpoint(DnsEndpoint endpoint)
+ {
+ return TestSkipHelpers.ShouldSkipEndpoint(endpoint, _output);
+ }
+
private async Task TryResolveWithDiagnostics(ClientX client, string domain, DnsRecordType recordType, int maxRetries = 3)
{
LogDiagnostics($"Attempting to resolve {domain} for record type {recordType}");
@@ -98,6 +103,7 @@ private async Task TryResolveWithDiagnostics(ClientX client, string
[InlineData(DnsEndpoint.OpenDNSFamily)]
public async Task ShouldWorkForTXTSync(DnsEndpoint endpoint)
{
+ if (ShouldSkipEndpoint(endpoint)) return;
using var client = new ClientX(endpoint);
var response = await TryResolveWithDiagnostics(client, "github.com", DnsRecordType.TXT);
@@ -127,8 +133,9 @@ public async Task ShouldWorkForTXTSync(DnsEndpoint endpoint)
[InlineData(DnsEndpoint.GoogleWireFormatPost)]
[InlineData(DnsEndpoint.OpenDNS)]
[InlineData(DnsEndpoint.OpenDNSFamily)]
- public async Task ShouldWorkForFirstSyncTXT(DnsEndpoint endpoint)
+ public async Task ShouldWorkForFirstSyncTXT(DnsEndpoint endpoint)
{
+ if (ShouldSkipEndpoint(endpoint)) return;
using var client = new ClientX(endpoint);
var response = await TryResolveWithDiagnostics(client, "github.com", DnsRecordType.TXT);
@@ -158,6 +165,7 @@ public async Task ShouldWorkForFirstSyncTXT(DnsEndpoint endpoint)
[InlineData(DnsEndpoint.OpenDNSFamily)]
public async Task ShouldWorkForAllSyncTXT(DnsEndpoint endpoint)
{
+ if (ShouldSkipEndpoint(endpoint)) return;
using var client = new ClientX(endpoint);
var response = await TryResolveWithDiagnostics(client, "github.com", DnsRecordType.TXT);
@@ -189,6 +197,7 @@ public async Task ShouldWorkForAllSyncTXT(DnsEndpoint endpoint)
[InlineData(DnsEndpoint.OpenDNSFamily)]
public async Task ShouldWorkForASync(DnsEndpoint endpoint)
{
+ if (ShouldSkipEndpoint(endpoint)) return;
using var client = new ClientX(endpoint);
var response = await TryResolveWithDiagnostics(client, "evotec.pl", DnsRecordType.A);
@@ -219,6 +228,7 @@ public async Task ShouldWorkForASync(DnsEndpoint endpoint)
[InlineData(DnsEndpoint.OpenDNS)]
[InlineData(DnsEndpoint.OpenDNSFamily)]
public void ShouldWorkForPTRSync(DnsEndpoint endpoint) {
+ if (ShouldSkipEndpoint(endpoint)) return;
using var client = new ClientX(endpoint);
var response = client.ResolveSync("1.1.1.1", DnsRecordType.PTR);
foreach (DnsAnswer answer in response.Answers) {
@@ -285,6 +295,7 @@ public void ShouldWorkForMultipleTypesSync(DnsEndpoint endpoint) {
[InlineData(DnsEndpoint.OpenDNS)]
[InlineData(DnsEndpoint.OpenDNSFamily)]
public void ShouldWorkForFirstSyncA(DnsEndpoint endpoint) {
+ if (ShouldSkipEndpoint(endpoint)) return;
using var client = new ClientX(endpoint);
var answer = client.ResolveFirstSync("evotec.pl", DnsRecordType.A, cancellationToken: CancellationToken.None);
Assert.True(answer != null);
@@ -311,6 +322,7 @@ public void ShouldWorkForFirstSyncA(DnsEndpoint endpoint) {
[InlineData(DnsEndpoint.OpenDNS)]
[InlineData(DnsEndpoint.OpenDNSFamily)]
public void ShouldWorkForAllSyncA(DnsEndpoint endpoint) {
+ if (ShouldSkipEndpoint(endpoint)) return;
using var client = new ClientX(endpoint);
var answers = client.ResolveAllSync("evotec.pl", DnsRecordType.A);
foreach (DnsAnswer answer in answers) {
diff --git a/DnsClientX.Tests/TestSkipHelpers.cs b/DnsClientX.Tests/TestSkipHelpers.cs
new file mode 100644
index 00000000..38e114cc
--- /dev/null
+++ b/DnsClientX.Tests/TestSkipHelpers.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Net;
+using Xunit.Abstractions;
+
+namespace DnsClientX.Tests;
+
+internal static class TestSkipHelpers
+{
+ internal static bool ShouldSkipEndpoint(DnsEndpoint endpoint, ITestOutputHelper? output = null)
+ {
+ return ShouldSkipSystemTcp(endpoint, output) || ShouldSkipOdoh(endpoint, output);
+ }
+
+ private static bool ShouldSkipSystemTcp(DnsEndpoint endpoint, ITestOutputHelper? output)
+ {
+ if (endpoint != DnsEndpoint.SystemTcp)
+ {
+ return false;
+ }
+
+ var servers = SystemInformation.GetDnsFromActiveNetworkCard();
+ if (servers == null || servers.Count == 0)
+ {
+ output?.WriteLine("[Diagnostic] System TCP DNS skipped: no active DNS servers detected.");
+ return true;
+ }
+
+ var allLoopback = true;
+ foreach (var server in servers)
+ {
+ if (IPAddress.TryParse(server, out var ip) && !IPAddress.IsLoopback(ip))
+ {
+ allLoopback = false;
+ break;
+ }
+ }
+
+ if (allLoopback)
+ {
+ output?.WriteLine("[Diagnostic] System TCP DNS skipped: only loopback resolvers detected.");
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool ShouldSkipOdoh(DnsEndpoint endpoint, ITestOutputHelper? output)
+ {
+ if (endpoint != DnsEndpoint.CloudflareOdoh)
+ {
+ return false;
+ }
+
+ var allow = Environment.GetEnvironmentVariable("DNSCLIENTX_RUN_ODOH_TESTS");
+ if (string.Equals(allow, "1", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(allow, "true", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ output?.WriteLine("[Diagnostic] Cloudflare ODoH tests skipped unless DNSCLIENTX_RUN_ODOH_TESTS=1.");
+ return true;
+ }
+}
diff --git a/DnsClientX/DnsClientX.ResolveFilter.cs b/DnsClientX/DnsClientX.ResolveFilter.cs
index b72a10a8..4151672b 100644
--- a/DnsClientX/DnsClientX.ResolveFilter.cs
+++ b/DnsClientX/DnsClientX.ResolveFilter.cs
@@ -28,10 +28,30 @@ public partial class ClientX {
/// Token used to cancel the operation.
/// A task that represents the asynchronous operation. The task result contains the DNS responses that match the filter.
public async Task ResolveFilter(string[] names, DnsRecordType type, string filter, bool requestDnsSec = false, bool validateDnsSec = false, bool retryOnTransient = true, int maxRetries = 3, int retryDelayMs = 100, CancellationToken cancellationToken = default) {
+ return await ResolveFilter(names, type, filter, new ResolveFilterOptions(), requestDnsSec, validateDnsSec, retryOnTransient, maxRetries, retryDelayMs, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Resolves multiple domain names for a single DNS record type in parallel using DNS over HTTPS.
+ /// This method allows you to specify a filter that will be applied to the data of the DNS answers.
+ ///
+ /// The domain names to resolve.
+ /// The type of DNS record to resolve.
+ /// The filter to apply to the DNS answers data.
+ /// Options for filtering, including alias inclusion.
+ /// Whether to request DNSSEC data in the response.
+ /// Whether to validate DNSSEC data.
+ /// Whether to retry on transient errors.
+ /// The maximum number of retries.
+ /// The delay between retries in milliseconds.
+ /// Token used to cancel the operation.
+ /// A task that represents the asynchronous operation. The task result contains the DNS responses that match the filter.
+ public async Task ResolveFilter(string[] names, DnsRecordType type, string filter, ResolveFilterOptions options, bool requestDnsSec = false, bool validateDnsSec = false, bool retryOnTransient = true, int maxRetries = 3, int retryDelayMs = 100, CancellationToken cancellationToken = default) {
+ filter ??= string.Empty;
int total = names.Length;
DnsResponse[] allResponses;
if (EndpointConfiguration.MaxConcurrency is null || EndpointConfiguration.MaxConcurrency <= 0 || EndpointConfiguration.MaxConcurrency >= total) {
- var tasksUnbounded = names.Select(name => Resolve(name, type, requestDnsSec, validateDnsSec, false, retryOnTransient, maxRetries, retryDelayMs, cancellationToken: cancellationToken)).ToList();
+ var tasksUnbounded = names.Select(name => Resolve(name, type, requestDnsSec, validateDnsSec, options.IncludeAliases, retryOnTransient, maxRetries, retryDelayMs, cancellationToken: cancellationToken)).ToList();
await Task.WhenAll(tasksUnbounded).ConfigureAwait(false);
allResponses = tasksUnbounded.Select(t => t.Result).ToArray();
} else {
@@ -44,7 +64,7 @@ public async Task ResolveFilter(string[] names, DnsRecordType typ
tasks.Add(Task.Run(async () => {
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try {
- allResponses[idx] = await Resolve(name, type, requestDnsSec, validateDnsSec, false, retryOnTransient, maxRetries, retryDelayMs, cancellationToken: cancellationToken).ConfigureAwait(false);
+ allResponses[idx] = await Resolve(name, type, requestDnsSec, validateDnsSec, options.IncludeAliases, retryOnTransient, maxRetries, retryDelayMs, cancellationToken: cancellationToken).ConfigureAwait(false);
} finally {
semaphore.Release();
}
@@ -54,9 +74,9 @@ public async Task ResolveFilter(string[] names, DnsRecordType typ
}
var filteredResponses = allResponses
- .Where(response => HasMatchingAnswers(response.Answers ?? Array.Empty(), filter, type))
+ .Where(response => HasMatchingAnswers(response.Answers ?? Array.Empty(), filter, type, options.IncludeAliases))
.Select(response => {
- response.Answers = FilterAnswers(response.Answers, filter, type);
+ response.Answers = FilterAnswers(response.Answers, filter, type, options.IncludeAliases);
return response;
})
.ToArray();
@@ -79,10 +99,29 @@ public async Task ResolveFilter(string[] names, DnsRecordType typ
/// Token used to cancel the operation.
/// A task that represents the asynchronous operation. The task result contains the DNS responses that match the filter.
public async Task ResolveFilter(string[] names, DnsRecordType type, Regex regexFilter, bool requestDnsSec = false, bool validateDnsSec = false, bool retryOnTransient = true, int maxRetries = 3, int retryDelayMs = 100, CancellationToken cancellationToken = default) {
+ return await ResolveFilter(names, type, regexFilter, new ResolveFilterOptions(), requestDnsSec, validateDnsSec, retryOnTransient, maxRetries, retryDelayMs, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Resolves multiple domain names for a single DNS record type in parallel using DNS over HTTPS.
+ /// This method allows you to specify a regular expression filter that will be applied to the data of the DNS answers.
+ ///
+ /// The domain names to resolve.
+ /// The type of DNS record to resolve.
+ /// The regular expression filter to apply to the DNS answers data.
+ /// Options for filtering, including alias inclusion.
+ /// Whether to request DNSSEC data in the response.
+ /// Whether to validate DNSSEC data.
+ /// Whether to retry on transient errors.
+ /// The maximum number of retries.
+ /// The delay between retries in milliseconds.
+ /// Token used to cancel the operation.
+ /// A task that represents the asynchronous operation. The task result contains the DNS responses that match the filter.
+ public async Task ResolveFilter(string[] names, DnsRecordType type, Regex regexFilter, ResolveFilterOptions options, bool requestDnsSec = false, bool validateDnsSec = false, bool retryOnTransient = true, int maxRetries = 3, int retryDelayMs = 100, CancellationToken cancellationToken = default) {
int total = names.Length;
DnsResponse[] allResponses;
if (EndpointConfiguration.MaxConcurrency is null || EndpointConfiguration.MaxConcurrency <= 0 || EndpointConfiguration.MaxConcurrency >= total) {
- var tasksUnbounded = names.Select(name => Resolve(name, type, requestDnsSec, validateDnsSec, false, retryOnTransient, maxRetries, retryDelayMs, cancellationToken: cancellationToken)).ToList();
+ var tasksUnbounded = names.Select(name => Resolve(name, type, requestDnsSec, validateDnsSec, options.IncludeAliases, retryOnTransient, maxRetries, retryDelayMs, cancellationToken: cancellationToken)).ToList();
await Task.WhenAll(tasksUnbounded).ConfigureAwait(false);
allResponses = tasksUnbounded.Select(t => t.Result).ToArray();
} else {
@@ -95,7 +134,7 @@ public async Task ResolveFilter(string[] names, DnsRecordType typ
tasks.Add(Task.Run(async () => {
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try {
- allResponses[idx] = await Resolve(name, type, requestDnsSec, validateDnsSec, false, retryOnTransient, maxRetries, retryDelayMs, cancellationToken: cancellationToken).ConfigureAwait(false);
+ allResponses[idx] = await Resolve(name, type, requestDnsSec, validateDnsSec, options.IncludeAliases, retryOnTransient, maxRetries, retryDelayMs, cancellationToken: cancellationToken).ConfigureAwait(false);
} finally {
semaphore.Release();
}
@@ -105,9 +144,9 @@ public async Task ResolveFilter(string[] names, DnsRecordType typ
}
var filteredResponses = allResponses
- .Where(response => HasMatchingAnswersRegex(response.Answers ?? Array.Empty(), regexFilter, type))
+ .Where(response => HasMatchingAnswersRegex(response.Answers ?? Array.Empty(), regexFilter, type, options.IncludeAliases))
.Select(response => {
- response.Answers = FilterAnswersRegex(response.Answers, regexFilter, type);
+ response.Answers = FilterAnswersRegex(response.Answers, regexFilter, type, options.IncludeAliases);
return response;
})
.ToArray();
@@ -131,10 +170,30 @@ public async Task ResolveFilter(string[] names, DnsRecordType typ
/// Token used to cancel the operation.
/// A task that represents the asynchronous operation. The task result contains the DNS response that matches the filter.
public async Task ResolveFilter(string name, DnsRecordType type, string filter, bool requestDnsSec = false, bool validateDnsSec = false, bool retryOnTransient = true, int maxRetries = 3, int retryDelayMs = 100, CancellationToken cancellationToken = default) {
- var response = await Resolve(name, type, requestDnsSec, validateDnsSec, false, retryOnTransient, maxRetries, retryDelayMs, cancellationToken: cancellationToken).ConfigureAwait(false);
+ return await ResolveFilter(name, type, filter, new ResolveFilterOptions(), requestDnsSec, validateDnsSec, retryOnTransient, maxRetries, retryDelayMs, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Resolves a single domain name for a single DNS record type using DNS over HTTPS.
+ /// This method allows you to specify a filter that will be applied to the data of the DNS answers.
+ ///
+ /// The domain name to resolve.
+ /// The type of DNS record to resolve.
+ /// The filter to apply to the DNS answers data.
+ /// Options for filtering, including alias inclusion.
+ /// Whether to request DNSSEC data in the response.
+ /// Whether to validate DNSSEC data.
+ /// Whether to retry on transient errors.
+ /// The maximum number of retries.
+ /// The delay between retries in milliseconds.
+ /// Token used to cancel the operation.
+ /// A task that represents the asynchronous operation. The task result contains the DNS response that matches the filter.
+ public async Task ResolveFilter(string name, DnsRecordType type, string filter, ResolveFilterOptions options, bool requestDnsSec = false, bool validateDnsSec = false, bool retryOnTransient = true, int maxRetries = 3, int retryDelayMs = 100, CancellationToken cancellationToken = default) {
+ filter ??= string.Empty;
+ var response = await Resolve(name, type, requestDnsSec, validateDnsSec, options.IncludeAliases, retryOnTransient, maxRetries, retryDelayMs, cancellationToken: cancellationToken).ConfigureAwait(false);
- if (!string.IsNullOrEmpty(filter) && response.Answers != null) {
- response.Answers = FilterAnswers(response.Answers, filter, type);
+ if (response.Answers != null && (options.IncludeAliases || !string.IsNullOrEmpty(filter))) {
+ response.Answers = FilterAnswers(response.Answers, filter, type, options.IncludeAliases);
}
return response;
@@ -155,10 +214,29 @@ public async Task ResolveFilter(string name, DnsRecordType type, st
/// Token used to cancel the operation.
/// A task that represents the asynchronous operation. The task result contains the DNS response that matches the filter.
public async Task ResolveFilter(string name, DnsRecordType type, Regex regexFilter, bool requestDnsSec = false, bool validateDnsSec = false, bool retryOnTransient = true, int maxRetries = 3, int retryDelayMs = 100, CancellationToken cancellationToken = default) {
- var response = await Resolve(name, type, requestDnsSec, validateDnsSec, false, retryOnTransient, maxRetries, retryDelayMs, cancellationToken: cancellationToken).ConfigureAwait(false);
+ return await ResolveFilter(name, type, regexFilter, new ResolveFilterOptions(), requestDnsSec, validateDnsSec, retryOnTransient, maxRetries, retryDelayMs, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Resolves a single domain name for a single DNS record type using DNS over HTTPS.
+ /// This method allows you to specify a regular expression filter that will be applied to the data of the DNS answers.
+ ///
+ /// The domain name to resolve.
+ /// The type of DNS record to resolve.
+ /// The regular expression filter to apply to the DNS answers data.
+ /// Options for filtering, including alias inclusion.
+ /// Whether to request DNSSEC data in the response.
+ /// Whether to validate DNSSEC data.
+ /// Whether to retry on transient errors.
+ /// The maximum number of retries.
+ /// The delay between retries in milliseconds.
+ /// Token used to cancel the operation.
+ /// A task that represents the asynchronous operation. The task result contains the DNS response that matches the filter.
+ public async Task ResolveFilter(string name, DnsRecordType type, Regex regexFilter, ResolveFilterOptions options, bool requestDnsSec = false, bool validateDnsSec = false, bool retryOnTransient = true, int maxRetries = 3, int retryDelayMs = 100, CancellationToken cancellationToken = default) {
+ var response = await Resolve(name, type, requestDnsSec, validateDnsSec, options.IncludeAliases, retryOnTransient, maxRetries, retryDelayMs, cancellationToken: cancellationToken).ConfigureAwait(false);
if (response.Answers != null) {
- response.Answers = FilterAnswersRegex(response.Answers, regexFilter, type);
+ response.Answers = FilterAnswersRegex(response.Answers, regexFilter, type, options.IncludeAliases);
}
return response;
@@ -172,6 +250,12 @@ public async Task ResolveFilter(string name, DnsRecordType type, Re
/// The DNS record type being filtered.
/// Filtered array of DNS answers.
private DnsAnswer[] FilterAnswers(DnsAnswer[] answers, string filter, DnsRecordType type) {
+ return FilterAnswers(answers, filter, type, false);
+ }
+
+ private DnsAnswer[] FilterAnswers(DnsAnswer[] answers, string filter, DnsRecordType type, bool includeAliases) {
+ filter ??= string.Empty;
+ var filterLower = filter.ToLowerInvariant();
var filteredAnswers = new List();
foreach (var answer in answers) {
@@ -179,10 +263,20 @@ private DnsAnswer[] FilterAnswers(DnsAnswer[] answers, string filter, DnsRecordT
continue;
}
+ var isAlias = IsAliasRecordType(answer.Type);
+ if (includeAliases && isAlias && answer.Type != type) {
+ filteredAnswers.Add(answer);
+ continue;
+ }
+
+ if (answer.Type != type) {
+ continue;
+ }
+
if (type == DnsRecordType.TXT && answer.Type == DnsRecordType.TXT) {
- // For TXT records, check if any line contains the filter
+ // For TXT records, check if any line contains the filter
var lines = answer.Data.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
- var matchingLines = lines.Where(line => line.ToLowerInvariant().Contains(filter.ToLowerInvariant())).ToArray();
+ var matchingLines = lines.Where(line => line.ToLowerInvariant().Contains(filterLower)).ToArray();
if (matchingLines.Length > 0) {
// Create a new answer with only the matching lines
@@ -198,7 +292,7 @@ private DnsAnswer[] FilterAnswers(DnsAnswer[] answers, string filter, DnsRecordT
}
} else {
// For non-TXT records, use the original logic
- if (answer.Data.ToLowerInvariant().Contains(filter.ToLowerInvariant())) {
+ if (answer.Data.ToLowerInvariant().Contains(filterLower)) {
filteredAnswers.Add(answer);
}
}
@@ -215,6 +309,10 @@ private DnsAnswer[] FilterAnswers(DnsAnswer[] answers, string filter, DnsRecordT
/// The DNS record type being filtered.
/// Filtered array of DNS answers.
private DnsAnswer[] FilterAnswersRegex(DnsAnswer[] answers, Regex regexFilter, DnsRecordType type) {
+ return FilterAnswersRegex(answers, regexFilter, type, false);
+ }
+
+ private DnsAnswer[] FilterAnswersRegex(DnsAnswer[] answers, Regex regexFilter, DnsRecordType type, bool includeAliases) {
var filteredAnswers = new List();
foreach (var answer in answers) {
@@ -222,6 +320,16 @@ private DnsAnswer[] FilterAnswersRegex(DnsAnswer[] answers, Regex regexFilter, D
continue;
}
+ var isAlias = IsAliasRecordType(answer.Type);
+ if (includeAliases && isAlias && answer.Type != type) {
+ filteredAnswers.Add(answer);
+ continue;
+ }
+
+ if (answer.Type != type) {
+ continue;
+ }
+
if (type == DnsRecordType.TXT && answer.Type == DnsRecordType.TXT) {
// For TXT records, check if any line matches the regex
var lines = answer.Data.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
@@ -258,23 +366,38 @@ private DnsAnswer[] FilterAnswersRegex(DnsAnswer[] answers, Regex regexFilter, D
/// The DNS record type being filtered.
/// True if any answer contains a match.
private bool HasMatchingAnswers(DnsAnswer[] answers, string filter, DnsRecordType type) {
+ return HasMatchingAnswers(answers, filter, type, false);
+ }
+
+ private bool HasMatchingAnswers(DnsAnswer[] answers, string filter, DnsRecordType type, bool includeAliases) {
if (answers == null) {
return false;
}
+ filter ??= string.Empty;
+ var filterLower = filter.ToLowerInvariant();
foreach (var answer in answers) {
if (string.IsNullOrEmpty(answer.Data)) {
continue;
}
+ var isAlias = IsAliasRecordType(answer.Type);
+ if (includeAliases && isAlias && answer.Type != type) {
+ return true;
+ }
+
+ if (answer.Type != type) {
+ continue;
+ }
+
if (type == DnsRecordType.TXT && answer.Type == DnsRecordType.TXT) {
var lines = answer.Data.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
- var matchingLines = lines.Where(line => line.ToLowerInvariant().Contains(filter.ToLowerInvariant())).ToArray();
+ var matchingLines = lines.Where(line => line.ToLowerInvariant().Contains(filterLower)).ToArray();
if (matchingLines.Length > 0) {
return true;
}
} else {
- if (answer.Data.ToLowerInvariant().Contains(filter.ToLowerInvariant())) {
+ if (answer.Data.ToLowerInvariant().Contains(filterLower)) {
return true;
}
}
@@ -290,6 +413,10 @@ private bool HasMatchingAnswers(DnsAnswer[] answers, string filter, DnsRecordTyp
/// The DNS record type being filtered.
/// True if any answer contains a match.
private bool HasMatchingAnswersRegex(DnsAnswer[] answers, Regex regexFilter, DnsRecordType type) {
+ return HasMatchingAnswersRegex(answers, regexFilter, type, false);
+ }
+
+ private bool HasMatchingAnswersRegex(DnsAnswer[] answers, Regex regexFilter, DnsRecordType type, bool includeAliases) {
if (answers == null) {
return false;
}
@@ -299,6 +426,15 @@ private bool HasMatchingAnswersRegex(DnsAnswer[] answers, Regex regexFilter, Dns
continue;
}
+ var isAlias = IsAliasRecordType(answer.Type);
+ if (includeAliases && isAlias && answer.Type != type) {
+ return true;
+ }
+
+ if (answer.Type != type) {
+ continue;
+ }
+
if (type == DnsRecordType.TXT && answer.Type == DnsRecordType.TXT) {
var lines = answer.Data.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
var matchingLines = lines.Where(line => regexFilter.IsMatch(line)).ToArray();
@@ -313,5 +449,9 @@ private bool HasMatchingAnswersRegex(DnsAnswer[] answers, Regex regexFilter, Dns
}
return false;
}
+
+ private static bool IsAliasRecordType(DnsRecordType recordType) {
+ return recordType == DnsRecordType.CNAME || recordType == DnsRecordType.DNAME;
+ }
}
}
diff --git a/DnsClientX/ResolveFilterOptions.cs b/DnsClientX/ResolveFilterOptions.cs
new file mode 100644
index 00000000..42838881
--- /dev/null
+++ b/DnsClientX/ResolveFilterOptions.cs
@@ -0,0 +1,13 @@
+namespace DnsClientX;
+
+///
+/// Provides options for
+/// and related overloads.
+///
+///
+///
+/// var options = new ResolveFilterOptions(IncludeAliases: true);
+/// var response = await client.ResolveFilter("example.com", DnsRecordType.TXT, "v=spf1", options);
+///
+///
+public readonly record struct ResolveFilterOptions(bool IncludeAliases = false);