Skip to content
This repository was archived by the owner on Dec 18, 2018. It is now read-only.

Bind to both IPv4 and IPv6 when localhost is specified (#231). #870

Merged
merged 1 commit into from
May 26, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ internal class Constants

public const int EOF = -4095;
public static readonly int? ECONNRESET = GetECONNRESET();
public static readonly int? EADDRINUSE = GetEADDRINUSE();

/// <summary>
/// Prefix of host name used to specify Unix sockets in the configuration.
Expand Down Expand Up @@ -39,5 +40,20 @@ internal class Constants
return null;
}
}

private static int? GetEADDRINUSE()
{
switch (PlatformServices.Default.Runtime.OperatingSystemPlatform)
{
case Platform.Windows:
return -4091;
case Platform.Linux:
return -98;
case Platform.Darwin:
return -48;
default:
return null;
}
}
}
}
71 changes: 69 additions & 2 deletions src/Microsoft.AspNetCore.Server.Kestrel/KestrelServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Reflection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Http;
using Microsoft.AspNetCore.Server.Kestrel.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Networking;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

Expand Down Expand Up @@ -94,6 +96,10 @@ public void Start<TContext>(IHttpApplication<TContext> application)
{
_logger.LogWarning("Unable to determine ECONNRESET value on this platform.");
}
if (!Constants.EADDRINUSE.HasValue)
{
_logger.LogWarning("Unable to determine EADDRINUSE value on this platform.");
}

engine.Start(threadCount);
var atLeastOneListener = false;
Expand All @@ -108,8 +114,69 @@ public void Start<TContext>(IHttpApplication<TContext> application)
else
{
atLeastOneListener = true;
_disposables.Push(engine.CreateServer(
parsedAddress));

if (!parsedAddress.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase))
{
_disposables.Push(engine.CreateServer(
parsedAddress));
}
else
{
if (parsedAddress.Port == 0)
{
throw new InvalidOperationException("Dynamic port binding is not supported when binding to localhost. You must either bind to 127.0.0.1:0 or [::1]:0, or both.");
}

var ipv4Address = parsedAddress.WithHost("127.0.0.1");
var exceptions = new List<UvException>();

try
{
_disposables.Push(engine.CreateServer(ipv4Address));
}
catch (AggregateException ex)
{
var uvException = ex.InnerException as UvException;

if (uvException != null && uvException.StatusCode != Constants.EADDRINUSE)
{
_logger.LogWarning(0, ex, $"Unable to bind to {parsedAddress.ToString()} on the IPv4 loopback interface.");
exceptions.Add(uvException);
}
else
{
throw;
}
}

var ipv6Address = parsedAddress.WithHost("[::1]");

try
{
_disposables.Push(engine.CreateServer(ipv6Address));
}
catch (AggregateException ex)
{
var uvException = ex.InnerException as UvException;

if (uvException != null && uvException.StatusCode != Constants.EADDRINUSE)
{
_logger.LogWarning(0, ex, $"Unable to bind to {parsedAddress.ToString()} on the IPv6 loopback interface.");
exceptions.Add(uvException);
}
else
{
throw;
}
}

if (exceptions.Count == 2)
{
var ex = new AggregateException(exceptions);
_logger.LogError(0, ex, $"Unable to bind to {parsedAddress.ToString()} on any loopback interface.");
throw ex;
}
}

// If requested port was "0", replace with assigned dynamic port.
_serverAddresses.Addresses.Remove(address);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public int Check(int statusCode, out Exception error)
{
var errorName = err_name(statusCode);
var errorDescription = strerror(statusCode);
error = new UvException("Error " + statusCode + " " + errorName + " " + errorDescription);
error = new UvException("Error " + statusCode + " " + errorName + " " + errorDescription, statusCode);
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Networking
{
public class UvException : Exception
{
public UvException(string message) : base(message) { }
public UvException(string message, int statusCode) : base(message)
{
StatusCode = statusCode;
}

public int StatusCode { get; }
}
}
11 changes: 11 additions & 0 deletions src/Microsoft.AspNetCore.Server.Kestrel/ServerAddress.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,5 +155,16 @@ public static ServerAddress FromUrl(string url)

return serverAddress;
}

internal ServerAddress WithHost(string host)
{
return new ServerAddress
{
Scheme = Scheme,
Host = host,
Port = Port,
PathBase = PathBase
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
Expand All @@ -13,8 +12,8 @@
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Server.Kestrel.Networking;
using Microsoft.AspNetCore.Testing.xunit;
using Microsoft.Extensions.Configuration;
using Xunit;

namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
Expand Down Expand Up @@ -42,26 +41,28 @@ public async Task RegisterAddresses_IPv6_Success(string addressInput, Func<IServ
await RegisterAddresses_Success(addressInput, testUrls);
}

[ConditionalTheory, MemberData(nameof(AddressRegistrationDataIPv6Port80))]
[IPv6SupportedCondition]
[Port80SupportedCondition]
public async Task RegisterAddresses_IPv6Port80_Success(string addressInput, Func<IServerAddressesFeature, string[]> testUrls)
{
await RegisterAddresses_Success(addressInput, testUrls);
}

[ConditionalTheory, MemberData(nameof(AddressRegistrationDataIPv6ScopeId))]
[IPv6SupportedCondition]
[OSSkipCondition(OperatingSystems.Linux, SkipReason = "HttpClient does not support IPv6 with scope ID on Linux (https://github.com/dotnet/corefx/issues/8235).")]
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "HttpClient does not support IPv6 with scope ID on Mac (https://github.com/dotnet/corefx/issues/8235).")]
public async Task RegisterAddresses_IPv6ScopeId_Success(string addressInput, Func<IServerAddressesFeature, string[]> testUrls)
{
await RegisterAddresses_Success(addressInput, testUrls);
}

public async Task RegisterAddresses_Success(string addressInput, Func<IServerAddressesFeature, string[]> testUrls)
private async Task RegisterAddresses_Success(string addressInput, Func<IServerAddressesFeature, string[]> testUrls)
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{ "server.urls", addressInput }
})
.Build();

var hostBuilder = new WebHostBuilder()
.UseConfiguration(config)
.UseKestrel()
.UseUrls(addressInput)
.Configure(ConfigureEchoAddress);

using (var host = hostBuilder.Build())
Expand All @@ -84,33 +85,85 @@ public async Task RegisterAddresses_Success(string addressInput, Func<IServerAdd
}
}

[Fact]
public void ThrowsWhenBindingLocalhostToIPv4AddressInUse()
{
ThrowsWhenBindingLocalhostToAddressInUse(AddressFamily.InterNetwork, IPAddress.Loopback);
}

[ConditionalFact]
[IPv6SupportedCondition]
public void ThrowsWhenBindingLocalhostToIPv6AddressInUse()
{
ThrowsWhenBindingLocalhostToAddressInUse(AddressFamily.InterNetworkV6, IPAddress.IPv6Loopback);
}

[Fact]
public void ThrowsWhenBindingLocalhostToDynamicPort()
{
var hostBuilder = new WebHostBuilder()
.UseKestrel()
.UseUrls("http://localhost:0")
.Configure(ConfigureEchoAddress);

using (var host = hostBuilder.Build())
{
Assert.Throws<InvalidOperationException>(() => host.Start());
}
}

private void ThrowsWhenBindingLocalhostToAddressInUse(AddressFamily addressFamily, IPAddress address)
{
using (var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp))
{
var port = GetNextPort();
socket.Bind(new IPEndPoint(address, port));

var hostBuilder = new WebHostBuilder()
.UseKestrel()
.UseUrls($"http://localhost:{port}")
.Configure(ConfigureEchoAddress);

using (var host = hostBuilder.Build())
{
var exception = Assert.Throws<AggregateException>(() => host.Start());
Assert.Contains(exception.InnerExceptions, ex => ex is UvException);
}
}
}

public static TheoryData<string, Func<IServerAddressesFeature, string[]>> AddressRegistrationDataIPv4
{
get
{
var dataset = new TheoryData<string, Func<IServerAddressesFeature, string[]>>();

// Default host and port
dataset.Add(null, _ => new[] { "http://localhost:5000/" });
dataset.Add(string.Empty, _ => new[] { "http://localhost:5000/" });
dataset.Add(null, _ => new[] { "http://127.0.0.1:5000/" });
dataset.Add(string.Empty, _ => new[] { "http://127.0.0.1:5000/" });

// Static port
// Static ports
var port1 = GetNextPort();
var port2 = GetNextPort();

// Ensure multiple addresses can be separated by semicolon
dataset.Add($"http://localhost:{port1};http://localhost:{port2}",
_ => new[] { $"http://localhost:{port1}/", $"http://localhost:{port2}/" });
// Loopback
dataset.Add($"http://127.0.0.1:{port1}", _ => new[] { $"http://127.0.0.1:{port1}/" });

// Ensure "localhost" and "127.0.0.1" are equivalent
// Localhost
dataset.Add($"http://localhost:{port1}", _ => new[] { $"http://localhost:{port1}/", $"http://127.0.0.1:{port1}/" });
dataset.Add($"http://127.0.0.1:{port1}", _ => new[] { $"http://localhost:{port1}/", $"http://127.0.0.1:{port1}/" });

// Any
dataset.Add($"http://*:{port1}/", _ => new[] { $"http://127.0.0.1:{port1}/" });
dataset.Add($"http://+:{port1}/", _ => new[] { $"http://127.0.0.1:{port1}/" });

// Multiple addresses
dataset.Add($"http://127.0.0.1:{port1};http://127.0.0.1:{port2}", _ => new[] { $"http://127.0.0.1:{port1}/", $"http://127.0.0.1:{port2}/" });

// Path after port
dataset.Add($"http://localhost:{port1}/base/path", _ => new[] { $"http://localhost:{port1}/base/path" });
dataset.Add($"http://127.0.0.1:{port1}/base/path", _ => new[] { $"http://127.0.0.1:{port1}/base/path" });

// Dynamic port
dataset.Add("http://localhost:0/", GetTestUrls);
// Dynamic port and non-loopback addresses
dataset.Add("http://127.0.0.1:0/", GetTestUrls);
dataset.Add($"http://{Dns.GetHostName()}:0/", GetTestUrls);

var ipv4Addresses = Dns.GetHostAddressesAsync(Dns.GetHostName()).Result
Expand All @@ -131,8 +184,9 @@ public static TheoryData<string, Func<IServerAddressesFeature, string[]>> Addres
var dataset = new TheoryData<string, Func<IServerAddressesFeature, string[]>>();

// Default port for HTTP (80)
dataset.Add("http://*", _ => new[] { "http://localhost/" });
dataset.Add("http://localhost", _ => new[] { "http://localhost/" });
dataset.Add("http://127.0.0.1", _ => new[] { "http://127.0.0.1/" });
dataset.Add("http://localhost", _ => new[] { "http://127.0.0.1/" });
dataset.Add("http://*", _ => new[] { "http://127.0.0.1/" });

return dataset;
}
Expand All @@ -144,17 +198,32 @@ public static TheoryData<string, Func<IServerAddressesFeature, string[]>> Addres
{
var dataset = new TheoryData<string, Func<IServerAddressesFeature, string[]>>();

// Static port
var port = GetNextPort();
dataset.Add($"http://*:{port}/", _ => new[] { $"http://localhost:{port}/", $"http://127.0.0.1:{port}/", $"http://[::1]:{port}/" });
dataset.Add($"http://localhost:{port}/", _ => new[] { $"http://localhost:{port}/", $"http://127.0.0.1:{port}/",
/* // https://github.com/aspnet/KestrelHttpServer/issues/231
$"http://[::1]:{port}/"
*/ });
dataset.Add($"http://[::1]:{port}/", _ => new[] { $"http://[::1]:{port}/", });
dataset.Add($"http://127.0.0.1:{port}/;http://[::1]:{port}/", _ => new[] { $"http://127.0.0.1:{port}/", $"http://[::1]:{port}/" });
// Default host and port
dataset.Add(null, _ => new[] { "http://127.0.0.1:5000/", "http://[::1]:5000/" });
dataset.Add(string.Empty, _ => new[] { "http://127.0.0.1:5000/", "http://[::1]:5000/" });

// Dynamic port
// Static ports
var port1 = GetNextPort();
var port2 = GetNextPort();

// Loopback
dataset.Add($"http://[::1]:{port1}/", _ => new[] { $"http://[::1]:{port1}/" });

// Localhost
dataset.Add($"http://localhost:{port1}", _ => new[] { $"http://localhost:{port1}/", $"http://127.0.0.1:{port1}/", $"http://[::1]:{port1}/" });

// Any
dataset.Add($"http://*:{port1}/", _ => new[] { $"http://127.0.0.1:{port1}/", $"http://[::1]:{port1}/" });
dataset.Add($"http://+:{port1}/", _ => new[] { $"http://127.0.0.1:{port1}/", $"http://[::1]:{port1}/" });

// Multiple addresses
dataset.Add($"http://127.0.0.1:{port1}/;http://[::1]:{port1}/", _ => new[] { $"http://127.0.0.1:{port1}/", $"http://[::1]:{port1}/" });
dataset.Add($"http://[::1]:{port1};http://[::1]:{port2}", _ => new[] { $"http://[::1]:{port1}/", $"http://[::1]:{port2}/" });

// Path after port
dataset.Add($"http://[::1]:{port1}/base/path", _ => new[] { $"http://[::1]:{port1}/base/path" });

// Dynamic port and non-loopback addresses
var ipv6Addresses = Dns.GetHostAddressesAsync(Dns.GetHostName()).Result
.Where(ip => ip.AddressFamily == AddressFamily.InterNetworkV6)
.Where(ip => ip.ScopeId == 0);
Expand All @@ -167,6 +236,21 @@ public static TheoryData<string, Func<IServerAddressesFeature, string[]>> Addres
}
}

public static TheoryData<string, Func<IServerAddressesFeature, string[]>> AddressRegistrationDataIPv6Port80
{
get
{
var dataset = new TheoryData<string, Func<IServerAddressesFeature, string[]>>();

// Default port for HTTP (80)
dataset.Add("http://[::1]", _ => new[] { "http://[::1]/" });
dataset.Add("http://localhost", _ => new[] { "http://127.0.0.1/", "http://[::1]/" });
dataset.Add("http://*", _ => new[] { "http://[::1]/" });

return dataset;
}
}

public static TheoryData<string, Func<IServerAddressesFeature, string[]>> AddressRegistrationDataIPv6ScopeId
{
get
Expand Down
Loading