Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix DNS over HTTPS failing to connect when any binding address contains IPAddress.IPV6Any #364

Merged
merged 8 commits into from
Jan 26, 2025
2 changes: 1 addition & 1 deletion src/Fluxzy.Core/Clients/Dns/DefaultDnsResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public async Task<IReadOnlyCollection<IPAddress>> SolveDnsAll(string hostName)

try
{
var result = (await InternalSolveDns(hostName));
var result = await InternalSolveDns(hostName).ConfigureAwait(false);
return _cache[hostName] = result.ToList();
}
catch (Exception ex)
Expand Down
95 changes: 61 additions & 34 deletions src/Fluxzy.Core/Clients/Dns/DnsOverHttpsResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,28 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Fluxzy.Core;
using Fluxzy.Utils;

namespace Fluxzy.Clients.Dns
{
internal class DnsOverHttpsResolver : DefaultDnsResolver
{
private static readonly Dictionary<string, string> DnsOverHttpsDefaultUrl = new(StringComparer.OrdinalIgnoreCase) {
["GOOGLE"] = Environment.GetEnvironmentVariable("FLUXZY_DEFAULT_GOOGLE_DNS_URL") ?? "https://dns.google/resolve",
["CLOUDFLARE"] = Environment.GetEnvironmentVariable("FLUXZY_DEFAULT_CLOUDFLARE_DNS_URL") ?? "https://cloudflare-dns.com/dns-query",
["GOOGLE"] = Environment.GetEnvironmentVariable("FLUXZY_DEFAULT_GOOGLE_DNS_URL") ?? "https://8.8.8.8/resolve",
["CLOUDFLARE"] = Environment.GetEnvironmentVariable("FLUXZY_DEFAULT_CLOUDFLARE_DNS_URL") ?? "https://1.1.1.1/dns-query",
};

private static readonly HashSet<string> _ByPassList = new(new[] { "localhost" }, StringComparer.OrdinalIgnoreCase);
private static readonly HashSet<string> ByPassList = new(new[] { "localhost" }, StringComparer.OrdinalIgnoreCase);

private static readonly HttpMessageHandler DefaultHandler = new HttpClientHandler() {
UseProxy = false,
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true
};

private readonly Dictionary<DnsCacheKey, IReadOnlyCollection<string?>> _cache = new();

private readonly string _finalUrl;
private readonly HttpClientHandler _clientHandler;
private readonly HttpClient _client;
private readonly string _host;

Expand All @@ -47,67 +55,73 @@ public DnsOverHttpsResolver(string nameOrUrl, ProxyConfiguration? proxyConfigura

_host = finalUrl.Host;

_clientHandler = new HttpClientHandler();
_client = new HttpClient(proxyConfiguration?.GetDefaultHandler() ?? DefaultHandler);
}

if (proxyConfiguration != null)
{
var webProxy = new WebProxy(proxyConfiguration.Host, proxyConfiguration.Port);
protected async Task<IReadOnlyCollection<string?>> GetDnsData(string type, string hostName)
{
var key = new DnsCacheKey(type, hostName);

if (proxyConfiguration.Credentials != null)
{
webProxy.Credentials = proxyConfiguration.Credentials;
}
using var _ = await Synchronizer<DnsCacheKey>.Instance.LockAsync(key).ConfigureAwait(false);

_clientHandler.Proxy = webProxy;
_clientHandler.UseProxy = true;
}
else
if (_cache.TryGetValue(key, out var cached))
{
_clientHandler.UseProxy = false;
return cached;
}

_client = new HttpClient(_clientHandler);
var result = await InternalGetDnsData(type, hostName);

_cache[key] = result;


return result;
}

protected async Task<IReadOnlyCollection<string?>> GetDnsData(string type, string hostName)
private async Task<List<string?>> InternalGetDnsData(string type, string hostName)
{
var requestMessage = new HttpRequestMessage(HttpMethod.Get, $"{_finalUrl}?name={hostName}&type={type}");

requestMessage.Headers.Add("Accept", "application/dns-json");

var response = await _client.SendAsync(requestMessage).ConfigureAwait(false);
using var response = await _client.SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead)
.ConfigureAwait(false);

if (!response.IsSuccessStatusCode)
{
throw new FluxzyException("Failed to resolve DNS over HTTPS");
}

var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var dnsResponse = JsonSerializer.Deserialize<DnsOverHttpsResponse>(content);
await using var content = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);

var dnsResponse = await JsonSerializer.DeserializeAsync<DnsOverHttpsResponse>(content).ConfigureAwait(false);

if (dnsResponse == null)
{
throw new FluxzyException("Invalid DNS response (null)");
throw new ClientErrorException(0, "Invalid DNS response (null)");
}

if (dnsResponse.Status != 0)
{
throw new FluxzyException($"Failed to resolve DNS over HTTPS. Status response = {dnsResponse.Status}");
throw new ClientErrorException(0,
$"Failed to resolve DNS over HTTPS. Status response = {dnsResponse.Status}");
}

return dnsResponse.Answers
.Where(a => a.Type == 1)
.Select(a => a.Data)
.ToList();
var result = dnsResponse.Answers
.Where(a => a.Type == 1)
.Select(a => a.Data)
.ToList();

return result;
}

protected override async Task<IEnumerable<IPAddress>> InternalSolveDns(string hostName)
{
if (string.Equals(hostName, _host, StringComparison.OrdinalIgnoreCase) ||
_ByPassList.Contains(hostName) || IPAddress.TryParse(hostName, out _)) {
if (string.Equals(hostName, _host, StringComparison.OrdinalIgnoreCase)
|| ByPassList.Contains(hostName)
|| IPAddress.TryParse(hostName, out _)
) {
// fallback to default resolver to avoid resolving loop
return await base.InternalSolveDns(hostName);
return await base.InternalSolveDns(hostName).ConfigureAwait(false);
}

// application/dns-json
Expand All @@ -123,13 +137,26 @@ protected override async Task<IEnumerable<IPAddress>> InternalSolveDns(string ho

if (!result.Any())
{
return await base.InternalSolveDns(hostName);
return await base.InternalSolveDns(hostName).ConfigureAwait(false);
}

return result;
}
}

internal record struct DnsCacheKey
{
public DnsCacheKey(string type, string host)
{
Type = type;
Host = host;
}

public string Type { get; }

public string Host { get; }
}

public class DnsOverHttpsAnswer
{
public DnsOverHttpsAnswer(string name, int? type, string? data)
Expand Down
3 changes: 2 additions & 1 deletion src/Fluxzy.Core/Clients/PoolBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ public async ValueTask<IHttpConnectionPool>

var dnsSolver = string.IsNullOrWhiteSpace(exchange.Context.DnsOverHttpsNameOrUrl) ?
_dnsSolver : _dnsSolversCache.GetOrAdd(exchange.Context.DnsOverHttpsNameOrUrl,
n => new DnsOverHttpsResolver(n, proxyRuntimeSetting.GetInternalProxyAuthentication()));
n => new DnsOverHttpsResolver(n, exchange.Context.DnsOverHttpsCapture ?
proxyRuntimeSetting.GetInternalProxyAuthentication() : null));

// We should solve DNS here
var computeDnsPromise =
Expand Down
36 changes: 36 additions & 0 deletions src/Fluxzy.Core/Clients/ProxyConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak

using System.Net;
using System.Net.Http;

namespace Fluxzy.Clients
{
Expand All @@ -9,6 +10,8 @@ namespace Fluxzy.Clients
/// </summary>
public class ProxyConfiguration
{
private HttpMessageHandler? _clientHandler;

public ProxyConfiguration(string host, int port, NetworkCredential? credentials)
{
Host = host;
Expand Down Expand Up @@ -49,5 +52,38 @@ public ProxyConfiguration(string host, int port, string? proxyAuthorizationHeade
/// The Proxy Authorization header value.
/// </value>
public string? ProxyAuthorizationHeader { get; set; }

/// <summary>
///
/// </summary>
/// <returns></returns>
internal HttpMessageHandler GetDefaultHandler()
{
if (_clientHandler != null) {
return _clientHandler;
}

lock (this) {

var clientHandler = new HttpClientHandler()
{
ServerCertificateCustomValidationCallback = (_, certificate2, arg3, arg4) => true,
};

var webProxy = new WebProxy(Host, Port);

if (Credentials != null)
{
webProxy.Credentials = Credentials;
}

clientHandler.Proxy = webProxy;
clientHandler.UseProxy = true;

_clientHandler = clientHandler;

return clientHandler;
}
}
}
}
132 changes: 66 additions & 66 deletions src/Fluxzy.Core/Core/Connection.cs
Original file line number Diff line number Diff line change
@@ -1,68 +1,68 @@
// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak

using System;
using System.IO;
using System.Net;
using System.Threading;
// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak
using System;
using System.IO;
using System.Net;
using System.Threading;
using Fluxzy.Clients;

namespace Fluxzy.Core
{
/// <summary>
/// Contains information about transport layer
/// </summary>
public class Connection : IRemoteLink
{
private int _requestProcessed;

public Connection(Authority authority, IIdProvider idProvider)
{
Authority = authority;
Id = idProvider.NextConnectionId();
}

public int Id { get; set; }

public string? HttpVersion { get; set; }

public int RequestProcessed => _requestProcessed;

public Authority Authority { get; set; }

public IPAddress? RemoteAddress { get; set; }

public SslInfo? SslInfo { get; set; }

public DateTime DnsSolveStart { get; set; }

public DateTime DnsSolveEnd { get; set; }

public DateTime TcpConnectionOpening { get; set; }

public DateTime TcpConnectionOpened { get; set; }

public DateTime SslNegotiationStart { get; set; }

public DateTime SslNegotiationEnd { get; set; }

public DateTime ProxyConnectStart { get; set; }

public DateTime ProxyConnectEnd { get; set; }


public int LocalPort { get; set; }

public string? LocalAddress { get; set; }

public Stream? WriteStream { get; set; }

public Stream? ReadStream { get; set; }

public int TimeoutIdleSeconds { get; set; } = -1;

public void AddNewRequestProcessed()
{
Interlocked.Increment(ref _requestProcessed);
}
}
}
namespace Fluxzy.Core
{
/// <summary>
/// Contains information about transport layer
/// </summary>
public class Connection : IRemoteLink
{
private int _requestProcessed;
public Connection(Authority authority, IIdProvider idProvider)
{
Authority = authority;
Id = idProvider.NextConnectionId();
}
public int Id { get; set; }
public string? HttpVersion { get; set; }
public int RequestProcessed => _requestProcessed;
public Authority Authority { get; set; }
public IPAddress? RemoteAddress { get; set; }
public SslInfo? SslInfo { get; set; }
public DateTime DnsSolveStart { get; set; }
public DateTime DnsSolveEnd { get; set; }
public DateTime TcpConnectionOpening { get; set; }
public DateTime TcpConnectionOpened { get; set; }
public DateTime SslNegotiationStart { get; set; }
public DateTime SslNegotiationEnd { get; set; }
public DateTime ProxyConnectStart { get; set; }
public DateTime ProxyConnectEnd { get; set; }
public int LocalPort { get; set; }
public string? LocalAddress { get; set; }
public Stream? WriteStream { get; set; }
public Stream? ReadStream { get; set; }
public int TimeoutIdleSeconds { get; set; } = -1;
public void AddNewRequestProcessed()
{
Interlocked.Increment(ref _requestProcessed);
}
}
}
Loading
Loading