Skip to content

HTTP/3: QUIC connection listener fixes #35258

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

Merged
merged 5 commits into from
Aug 12, 2021
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
26 changes: 24 additions & 2 deletions src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Diagnostics;
using System.IO.Pipelines;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
Expand Down Expand Up @@ -203,6 +204,8 @@ async Task OnBind(ListenOptions options, CancellationToken onBindCancellationTok
// multiplexed transport factory, which happens if QUIC isn't supported.
var addAltSvcHeader = !options.DisableAltSvcHeader && _multiplexedTransportFactory != null;

var originalEndPoint = options.EndPoint;

// Add the HTTP middleware as the terminal connection middleware
if (hasHttp1 || hasHttp2
|| options.Protocols == HttpProtocols.None) // TODO a test fails because it doesn't throw an exception in the right place
Expand All @@ -219,7 +222,7 @@ async Task OnBind(ListenOptions options, CancellationToken onBindCancellationTok
// Add the connection limit middleware
connectionDelegate = EnforceConnectionLimit(connectionDelegate, Options.Limits.MaxConcurrentConnections, Trace);

options.EndPoint = await _transportManager.BindAsync(options.EndPoint, connectionDelegate, options.EndpointConfig, onBindCancellationToken).ConfigureAwait(false);
options.EndPoint = await _transportManager.BindAsync(ResolveEndPoint(originalEndPoint, options.EndPoint), connectionDelegate, options.EndpointConfig, onBindCancellationToken).ConfigureAwait(false);
}

if (hasHttp3 && _multiplexedTransportFactory is not null)
Expand All @@ -230,7 +233,7 @@ async Task OnBind(ListenOptions options, CancellationToken onBindCancellationTok
// Add the connection limit middleware
multiplexedConnectionDelegate = EnforceConnectionLimit(multiplexedConnectionDelegate, Options.Limits.MaxConcurrentConnections, Trace);

options.EndPoint = await _transportManager.BindAsync(options.EndPoint, multiplexedConnectionDelegate, options, onBindCancellationToken).ConfigureAwait(false);
options.EndPoint = await _transportManager.BindAsync(ResolveEndPoint(originalEndPoint, options.EndPoint), multiplexedConnectionDelegate, options, onBindCancellationToken).ConfigureAwait(false);
}
}

Expand All @@ -247,6 +250,25 @@ async Task OnBind(ListenOptions options, CancellationToken onBindCancellationTok

// Register the options with the event source so it can be logged (if necessary)
KestrelEventSource.Log.AddServerOptions(Options);

static EndPoint ResolveEndPoint(EndPoint originalEndPoint, EndPoint currentEndPoint)
{
// From the original endpoint we want the address. This address could be a constant.
// For example, IPAddress.IPv4Any or IPAddress.IPv6Any. QUIC connection listener
// has logic that special cases these constants.
//
// However, from the current endpoint we want to get the port. If there is a port of 0
// then the first listener will resolve it to an actual port number. We want additional
// listeners to use that port number instead of each using 0 and resolving to different
// port numbers.
if (originalEndPoint is IPEndPoint originalIP && originalIP.Port == 0 &&
currentEndPoint is IPEndPoint currentIP && currentIP.Port != 0)
{
return new IPEndPoint(originalIP.Address, currentIP.Port);
}

return originalEndPoint;
}
}

// Graceful shutdown if possible
Expand Down
116 changes: 115 additions & 1 deletion src/Servers/Kestrel/Core/test/KestrelServerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Connections;
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.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
Expand Down Expand Up @@ -247,6 +249,78 @@ public void StartWithMultipleTransportFactoriesDoesNotThrow()
StartDummyApplication(server);
}

[Fact]
public async Task ListenIPWithStaticPort_TransportsGetIPv6Any()
{
var options = new KestrelServerOptions();
options.ApplicationServices = new ServiceCollection()
.AddLogging()
.BuildServiceProvider();
options.ListenAnyIP(5000, options =>
{
options.UseHttps();
options.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
});

var mockTransportFactory = new MockTransportFactory();
var mockMultiplexedTransportFactory = new MockMultiplexedTransportFactory();

using var server = new KestrelServerImpl(
Options.Create(options),
new List<IConnectionListenerFactory>() { mockTransportFactory },
new List<IMultiplexedConnectionListenerFactory>() { mockMultiplexedTransportFactory },
new LoggerFactory(new[] { new KestrelTestLoggerProvider() }));

await server.StartAsync(new DummyApplication(context => Task.CompletedTask), CancellationToken.None);

var transportEndPoint = Assert.Single(mockTransportFactory.BoundEndPoints);
var multiplexedTransportEndPoint = Assert.Single(mockMultiplexedTransportFactory.BoundEndPoints);

// Both transports should get the IPv6Any
Assert.Same(IPAddress.IPv6Any, ((IPEndPoint)transportEndPoint.OriginalEndPoint).Address);
Assert.Same(IPAddress.IPv6Any, ((IPEndPoint)multiplexedTransportEndPoint.OriginalEndPoint).Address);

Assert.Equal(5000, ((IPEndPoint)transportEndPoint.OriginalEndPoint).Port);
Assert.Equal(5000, ((IPEndPoint)multiplexedTransportEndPoint.OriginalEndPoint).Port);
}

[Fact]
public async Task ListenIPWithEphemeralPort_TransportsGetIPv6Any()
{
var options = new KestrelServerOptions();
options.ApplicationServices = new ServiceCollection()
.AddLogging()
.BuildServiceProvider();
options.ListenAnyIP(0, options =>
{
options.UseHttps();
options.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
});

var mockTransportFactory = new MockTransportFactory();
var mockMultiplexedTransportFactory = new MockMultiplexedTransportFactory();

using var server = new KestrelServerImpl(
Options.Create(options),
new List<IConnectionListenerFactory>() { mockTransportFactory },
new List<IMultiplexedConnectionListenerFactory>() { mockMultiplexedTransportFactory },
new LoggerFactory(new[] { new KestrelTestLoggerProvider() }));

await server.StartAsync(new DummyApplication(context => Task.CompletedTask), CancellationToken.None);

var transportEndPoint = Assert.Single(mockTransportFactory.BoundEndPoints);
var multiplexedTransportEndPoint = Assert.Single(mockMultiplexedTransportFactory.BoundEndPoints);

Assert.Same(IPAddress.IPv6Any, ((IPEndPoint)transportEndPoint.OriginalEndPoint).Address);
Assert.Same(IPAddress.IPv6Any, ((IPEndPoint)multiplexedTransportEndPoint.OriginalEndPoint).Address);

// Should have been assigned a random value.
Assert.NotEqual(0, ((IPEndPoint)transportEndPoint.BoundEndPoint).Port);

// Same random value should be used for both transports.
Assert.Equal(((IPEndPoint)transportEndPoint.BoundEndPoint).Port, ((IPEndPoint)multiplexedTransportEndPoint.BoundEndPoint).Port);
}

[Fact]
public async Task StopAsyncCallsCompleteWhenFirstCallCompletes()
{
Expand Down Expand Up @@ -692,10 +766,24 @@ private static void StartDummyApplication(IServer server)

private class MockTransportFactory : IConnectionListenerFactory
{
public List<BindDetail> BoundEndPoints { get; } = new List<BindDetail>();

public ValueTask<IConnectionListener> BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default)
{
EndPoint resolvedEndPoint = endpoint;
if (resolvedEndPoint is IPEndPoint ipEndPoint)
{
var port = ipEndPoint.Port == 0
? Random.Shared.Next(IPEndPoint.MinPort, IPEndPoint.MaxPort)
: ipEndPoint.Port;

resolvedEndPoint = new IPEndPoint(new IPAddress(ipEndPoint.Address.GetAddressBytes()), port);
}

BoundEndPoints.Add(new BindDetail(endpoint, resolvedEndPoint));

var mock = new Mock<IConnectionListener>();
mock.Setup(m => m.EndPoint).Returns(endpoint);
mock.Setup(m => m.EndPoint).Returns(resolvedEndPoint);
return new ValueTask<IConnectionListener>(mock.Object);
}
}
Expand All @@ -707,5 +795,31 @@ public ValueTask<IConnectionListener> BindAsync(EndPoint endpoint, CancellationT
throw new InvalidOperationException();
}
}

private class MockMultiplexedTransportFactory : IMultiplexedConnectionListenerFactory
{
public List<BindDetail> BoundEndPoints { get; } = new List<BindDetail>();

public ValueTask<IMultiplexedConnectionListener> BindAsync(EndPoint endpoint, IFeatureCollection features = null, CancellationToken cancellationToken = default)
{
EndPoint resolvedEndPoint = endpoint;
if (resolvedEndPoint is IPEndPoint ipEndPoint)
{
var port = ipEndPoint.Port == 0
? Random.Shared.Next(IPEndPoint.MinPort, IPEndPoint.MaxPort)
: ipEndPoint.Port;

resolvedEndPoint = new IPEndPoint(new IPAddress(ipEndPoint.Address.GetAddressBytes()), port);
}

BoundEndPoints.Add(new BindDetail(endpoint, resolvedEndPoint));

var mock = new Mock<IMultiplexedConnectionListener>();
mock.Setup(m => m.EndPoint).Returns(resolvedEndPoint);
return new ValueTask<IMultiplexedConnectionListener>(mock.Object);
}
}

private record BindDetail(EndPoint OriginalEndPoint, EndPoint BoundEndPoint);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ public async Task POST_ServerCompletesWithoutReadingRequestBody_ClientGetsRespon
using (var host = builder.Build())
using (var client = Http3Helpers.CreateClient())
{
await host.StartAsync();
await host.StartAsync().DefaultTimeout();

var requestContent = new StreamingHttpContext();

Expand All @@ -289,22 +289,22 @@ public async Task POST_ServerCompletesWithoutReadingRequestBody_ClientGetsRespon
// Act
var responseTask = client.SendAsync(request, CancellationToken.None);

var requestStream = await requestContent.GetStreamAsync();
var requestStream = await requestContent.GetStreamAsync().DefaultTimeout();

// Send headers
await requestStream.FlushAsync();
await requestStream.FlushAsync().DefaultTimeout();
// Write content
await requestStream.WriteAsync(TestData);
await requestStream.WriteAsync(TestData).DefaultTimeout();

var response = await responseTask;
var response = await responseTask.DefaultTimeout();

// Assert
response.EnsureSuccessStatusCode();
Assert.Equal(HttpVersion.Version30, response.Version);
var responseText = await response.Content.ReadAsStringAsync();
var responseText = await response.Content.ReadAsStringAsync().DefaultTimeout();
Assert.Equal("Hello world", responseText);

await host.StopAsync();
await host.StopAsync().DefaultTimeout();
}
}

Expand Down