Skip to content

Commit

Permalink
Respect multiple DNS resolutions in EndPointManager
Browse files Browse the repository at this point in the history
Updates EndPointManager to bind to *all* returned DNS records for hostname-based prefixes, not just the first one. This does diverge from expected Mono behavior, but should better match that of `Http.sys`.

Fixes unosquare#576.
  • Loading branch information
KazWolfe committed Apr 15, 2023
1 parent 2305190 commit 741856d
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 15 deletions.
39 changes: 24 additions & 15 deletions src/EmbedIO/Net/EndPointManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,28 +79,36 @@ internal static void AddPrefix(string p, HttpListener listener)
}

// listens on all the interfaces if host name cannot be parsed by IPAddress.
var epl = GetEpListener(lp.Host, lp.Port, listener, lp.Secure);
epl.AddPrefix(lp, listener);
var endPointListeners = GetEpListeners(lp.Host, lp.Port, listener, lp.Secure);
foreach (var epl in endPointListeners) {
epl.AddPrefix(lp, listener);
}
}

private static EndPointListener GetEpListener(string host, int port, HttpListener listener, bool secure = false)
private static IEnumerable<EndPointListener> GetEpListeners(string host, int port, HttpListener listener, bool secure = false)
{
var address = ResolveAddress(host);
var addresses = ResolveAddresses(host);
var endPointListeners = new List<EndPointListener>();

foreach (var address in addresses)
{
var p = IPToEndpoints.GetOrAdd(address, x => new ConcurrentDictionary<int, EndPointListener>());
endPointListeners.Add(p.GetOrAdd(port, x => new EndPointListener(listener, address, x, secure)));
}

var p = IPToEndpoints.GetOrAdd(address, x => new ConcurrentDictionary<int, EndPointListener>());
return p.GetOrAdd(port, x => new EndPointListener(listener, address, x, secure));
return endPointListeners;
}

private static IPAddress ResolveAddress(string host)
private static IEnumerable<IPAddress> ResolveAddresses(string host)
{
if (host == "*" || host == "+" || host == "0.0.0.0")
{
return UseIpv6 ? IPAddress.IPv6Any : IPAddress.Any;
return new[] { UseIpv6 ? IPAddress.IPv6Any : IPAddress.Any };
}

if (IPAddress.TryParse(host, out var address))
{
return address;
return new[] { address };
}

try
Expand All @@ -110,11 +118,10 @@ private static IPAddress ResolveAddress(string host)
AddressList = Dns.GetHostAddresses(host),
};

return hostEntry.AddressList[0];
return hostEntry.AddressList;
}
catch
{
return UseIpv6 ? IPAddress.IPv6Any : IPAddress.Any;
catch {
return new[] {UseIpv6 ? IPAddress.IPv6Any : IPAddress.Any};
}
}

Expand All @@ -129,8 +136,10 @@ private static void RemovePrefix(string prefix, HttpListener listener)
return;
}

var epl = GetEpListener(lp.Host, lp.Port, listener, lp.Secure);
epl.RemovePrefix(lp);
var epls = GetEpListeners(lp.Host, lp.Port, listener, lp.Secure);
foreach (var epl in epls) {
epl.RemovePrefix(lp);
}
}
catch (SocketException)
{
Expand Down
82 changes: 82 additions & 0 deletions test/EmbedIO.Tests/Issues/Issue576_LocalhostDualStack.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Threading.Tasks;
using NUnit.Framework;
using Swan;

namespace EmbedIO.Tests.Issues;

[TestFixture]
public class Issue576_LocalhostDualStack {
[TestCase("127.0.0.1")]
[TestCase("::1")]
public async Task LocalhostAcceptsDualStack(string address) {
if (SwanRuntime.OS != Swan.OperatingSystem.Windows)
Assert.Ignore("Only Windows");

using var instance = new WebServer(HttpListenerMode.EmbedIO, "http://localhost:8877");
instance.OnAny(ctx => ctx.SendDataAsync(DateTime.Now));

_ = instance.RunAsync();

using var handler = BuildFakeResolver(IPAddress.Parse(address));
using var client = new HttpClient(handler);

var uri = new Uri("http://localhost:8877");
Assert.IsNotEmpty(await client.GetStringAsync(uri).ConfigureAwait(false));
}

[TestCase("http://localhost:8877")]
[TestCase("http://127.0.0.1:8877")]
public async Task LocalhostV4AcceptsIpAndHost(string uri) {
if (SwanRuntime.OS != Swan.OperatingSystem.Windows)
Assert.Ignore("Only Windows");

using var instance = new WebServer(HttpListenerMode.EmbedIO, "http://localhost:8877", "http://127.0.0.1:8877");
instance.OnAny(ctx => ctx.SendDataAsync(DateTime.Now));

_ = instance.RunAsync();

using var handler = BuildFakeResolver(IPAddress.Loopback); // force ipv4 for this test
using var client = new HttpClient(handler);

Assert.IsNotEmpty(await client.GetStringAsync(new Uri(uri)).ConfigureAwait(false));
}

[TestCase("http://localhost:8877")]
[TestCase("http://[::1]:8877")]
public async Task LocalhostV6AcceptsIpAndHost(string uri) {
if (SwanRuntime.OS != Swan.OperatingSystem.Windows)
Assert.Ignore("Only Windows");

using var instance = new WebServer(HttpListenerMode.EmbedIO, "http://localhost:8877", "http://[::1]:8877");
instance.OnAny(ctx => ctx.SendDataAsync(DateTime.Now));

_ = instance.RunAsync();

using var handler = BuildFakeResolver(IPAddress.IPv6Loopback); // force ipv6 for this test
using var client = new HttpClient(handler);

Assert.IsNotEmpty(await client.GetStringAsync(new Uri(uri)).ConfigureAwait(false));
}

private static SocketsHttpHandler BuildFakeResolver(IPAddress target) {
// Borrowed from https://www.meziantou.net/forcing-httpclient-to-use-ipv4-or-ipv6-addresses.htm and adapted
return new SocketsHttpHandler {
ConnectCallback = async (context, cancellationToken) => {
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
socket.NoDelay = true;
try {
await socket.ConnectAsync(target, context.DnsEndPoint.Port, cancellationToken).ConfigureAwait(false);
return new NetworkStream(socket, ownsSocket: true);
} catch {
socket.Dispose();
throw;
}
},
};
}
}

0 comments on commit 741856d

Please sign in to comment.