Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
<!-- external dependencies -->
<PackageVersion Include="Confluent.Kafka" Version="2.10.1" />
<PackageVersion Include="Dapper" Version="2.1.66" />
<PackageVersion Include="DnsClient" Version="1.8.0" />
<PackageVersion Include="Google.Protobuf" Version="3.31.1" />
<PackageVersion Include="Grpc.AspNetCore" Version="2.71.0" />
<PackageVersion Include="Grpc.Net.ClientFactory" Version="2.71.0" />
Expand Down
102 changes: 102 additions & 0 deletions src/Microsoft.Extensions.ServiceDiscovery.Dns/FallbackDnsResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using DnsClient;
using DnsClient.Protocol;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver;

namespace Microsoft.Extensions.ServiceDiscovery.Dns;

internal sealed class FallbackDnsResolver : IDnsResolver
{
private readonly LookupClient _lookupClient;
private readonly IOptionsMonitor<DnsServiceEndpointProviderOptions> _options;
private readonly TimeProvider _timeProvider;

public FallbackDnsResolver(LookupClient lookupClient, IOptionsMonitor<DnsServiceEndpointProviderOptions> options, TimeProvider timeProvider)
{
_lookupClient = lookupClient;
_options = options;
_timeProvider = timeProvider;
}

private TimeSpan DefaultRefreshPeriod => _options.CurrentValue.DefaultRefreshPeriod;

public async ValueTask<AddressResult[]> ResolveIPAddressesAsync(string name, CancellationToken cancellationToken = default)
{
DateTime expiresAt = _timeProvider.GetUtcNow().DateTime.Add(DefaultRefreshPeriod);
var addresses = await System.Net.Dns.GetHostAddressesAsync(name, cancellationToken).ConfigureAwait(false);

var results = new AddressResult[addresses.Length];

for (int i = 0; i < addresses.Length; i++)
{
results[i] = new AddressResult
{
Address = addresses[i],
ExpiresAt = expiresAt
};
}

return results;
}

public async ValueTask<ServiceResult[]> ResolveServiceAsync(string name, CancellationToken cancellationToken = default)
{
DateTime now = _timeProvider.GetUtcNow().DateTime;
var queryResult = await _lookupClient.QueryAsync(name, DnsClient.QueryType.SRV, cancellationToken: cancellationToken).ConfigureAwait(false);
if (queryResult.HasError)
{
throw CreateException(name, queryResult.ErrorMessage);
}

var lookupMapping = new Dictionary<string, List<AddressResult>>();
foreach (var record in queryResult.Additionals.OfType<AddressRecord>())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We checked CNameRecord before:
foreach (var record in result.Additionals.Where(x => x is AddressRecord or CNameRecord))

Is that something we still should handle here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RFC explicitly prohibits aliases in the target of the SRV record

Target
The domain name of the target host. There MUST be one or more
address records for this name, the name MUST NOT be an alias (in
the sense of RFC 1034 or RFC 2181). Implementors are urged, but
not required, to return the address record(s) in the Additional
Data section. Unless and until permitted by future standards
action, name compression is not to be used for this field.

So we shouldn't need to look for CNAME records.

{
if (!lookupMapping.TryGetValue(record.DomainName, out var addresses))
{
addresses = new List<AddressResult>();
lookupMapping[record.DomainName] = addresses;
}

addresses.Add(new AddressResult
{
Address = record.Address,
ExpiresAt = now.Add(TimeSpan.FromSeconds(record.TimeToLive))
});
}

var srvRecords = queryResult.Answers.OfType<SrvRecord>().ToList();

var results = new ServiceResult[srvRecords.Count];
for (int i = 0; i < srvRecords.Count; i++)
{
var record = srvRecords[i];

results[i] = new ServiceResult
{
ExpiresAt = now.Add(TimeSpan.FromSeconds(record.TimeToLive)),
Priority = record.Priority,
Weight = record.Weight,
Port = record.Port,
Target = record.Target,
Addresses = lookupMapping.TryGetValue(record.Target, out var addresses)
? addresses.ToArray()
: Array.Empty<AddressResult>()
};
}

return results;
}

private static InvalidOperationException CreateException(string dnsName, string errorMessage)
{
var msg = errorMessage switch
{
{ Length: > 0 } => $"No DNS SRV records were found for DNS name '{dnsName}': {errorMessage}.",
_ => $"No DNS SRV records were found for DNS name '{dnsName}'",
};
return new InvalidOperationException(msg);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="DnsClient" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Options" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Net.Sockets;

namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver;

internal interface IDnsResolver
{
ValueTask<AddressResult[]> ResolveIPAddressesAsync(string name, AddressFamily addressFamily, CancellationToken cancellationToken = default);
ValueTask<AddressResult[]> ResolveIPAddressesAsync(string name, CancellationToken cancellationToken = default);
ValueTask<ServiceResult[]> ResolveServiceAsync(string name, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,38 @@ public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceC
ArgumentNullException.ThrowIfNull(configureOptions);

services.AddServiceDiscoveryCore();

if (!GetDnsClientFallbackFlag())
{
services.TryAddSingleton<IDnsResolver, DnsResolver>();
}
else
{
services.TryAddSingleton<IDnsResolver, FallbackDnsResolver>();
services.TryAddSingleton<DnsClient.LookupClient>();
}

services.TryAddSingleton<IDnsResolver, DnsResolver>();
services.AddSingleton<IServiceEndpointProviderFactory, DnsSrvServiceEndpointProviderFactory>();
var options = services.AddOptions<DnsSrvServiceEndpointProviderOptions>();
options.Configure(o => configureOptions?.Invoke(o));
return services;

static bool GetDnsClientFallbackFlag()
{
if (AppContext.TryGetSwitch("Microsoft.Extensions.ServiceDiscovery.Dns.UseDnsClientFallback", out var value))
{
return value;
}

var envVar = Environment.GetEnvironmentVariable("MICROSOFT_EXTENSIONS_SERVICE_DISCOVERY_DNS_USE_DNSCLIENT_FALLBACK");
if (envVar is not null && (envVar.Equals("true", StringComparison.OrdinalIgnoreCase) || envVar.Equals("1")))
Comment on lines +67 to +73
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't find an example how you do feature flags in Aspire, so I used the same mechanism as we use in .NET runtime.

I am open to name suggestions, I honestly could not think of a better name.

{
return true;
}

return false;
}
}

/// <summary>
Expand Down
Loading