Skip to content

Support side-by-side transports in Kestrel #44657

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 6 commits into from
Nov 1, 2022
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
@@ -0,0 +1,23 @@
// 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;

namespace Microsoft.AspNetCore.Connections;

/// <summary>
/// Defines an interface that determines whether the listener factory supports binding to the specified <see cref="EndPoint"/>.
/// </summary>
/// <remarks>
/// This interface should be implemented by <see cref="IConnectionListenerFactory"/> and <see cref="IMultiplexedConnectionListenerFactory"/>
/// types that want to control want endpoint instances they can bind to.
/// </remarks>
public interface IConnectionListenerFactorySelector
Copy link
Member

@Tratcher Tratcher Nov 1, 2022

Choose a reason for hiding this comment

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

Should we implement this on the socket transport and restrict it to known endpoints?

{
/// <summary>
/// Returns a value that indicates whether the listener factory supports binding to the specified <see cref="EndPoint"/>.
/// </summary>
/// <param name="endpoint">The <see cref="EndPoint" /> to bind to.</param>
/// <returns>A value that indicates whether the listener factory supports binding to the specified <see cref="EndPoint"/>.</returns>
bool CanBind(EndPoint endpoint);
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#nullable enable
Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature
Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature.OnClosed(System.Action<object?>! callback, object? state) -> void
Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector
Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector.CanBind(System.Net.EndPoint! endpoint) -> bool
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#nullable enable
Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature
Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature.OnClosed(System.Action<object?>! callback, object? state) -> void
Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector
Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector.CanBind(System.Net.EndPoint! endpoint) -> bool
Copy link
Member

Choose a reason for hiding this comment

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

@dotnet/aspnet-build It looks like we need to Update our PublicAPI baselines now that we're adding new APIs for .NET 8.

We'll have to be careful not to include IPNetwork.Parse/TryParse and RequestLocalizationOptions.CultureInfoUseUserOverride in the .NET 7 baseline. I think those are the only APIs we've added since branching for .NET 8.

Copy link
Contributor

Choose a reason for hiding this comment

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

Once 7.0.0 ships, we'll update the baselines. But not until then. That'll involve changes in release/7.0 and copying the Shipped files into main, then patching things up to reflect the new APIs in Unshipped files.

Microsoft.AspNetCore.Connections.TlsConnectionCallbackContext
Microsoft.AspNetCore.Connections.TlsConnectionCallbackContext.ClientHelloInfo.get -> System.Net.Security.SslClientHelloInfo
Microsoft.AspNetCore.Connections.TlsConnectionCallbackContext.ClientHelloInfo.set -> void
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#nullable enable
Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature
Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature.OnClosed(System.Action<object?>! callback, object? state) -> void
Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector
Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector.CanBind(System.Net.EndPoint! endpoint) -> bool
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#nullable enable
Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature
Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature.OnClosed(System.Action<object?>! callback, object? state) -> void
Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector
Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector.CanBind(System.Net.EndPoint! endpoint) -> bool
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@ internal sealed class TransportManager
{
private readonly List<ActiveTransport> _transports = new List<ActiveTransport>();

private readonly IConnectionListenerFactory? _transportFactory;
private readonly IMultiplexedConnectionListenerFactory? _multiplexedTransportFactory;
private readonly List<IConnectionListenerFactory> _transportFactories;
private readonly List<IMultiplexedConnectionListenerFactory> _multiplexedTransportFactories;
private readonly ServiceContext _serviceContext;

public TransportManager(
IConnectionListenerFactory? transportFactory,
IMultiplexedConnectionListenerFactory? multiplexedTransportFactory,
List<IConnectionListenerFactory> transportFactories,
List<IMultiplexedConnectionListenerFactory> multiplexedTransportFactories,
ServiceContext serviceContext)
{
_transportFactory = transportFactory;
_multiplexedTransportFactory = multiplexedTransportFactory;
_transportFactories = transportFactories;
_multiplexedTransportFactories = multiplexedTransportFactories;
_serviceContext = serviceContext;
}

Expand All @@ -37,19 +37,28 @@ public TransportManager(

public async Task<EndPoint> BindAsync(EndPoint endPoint, ConnectionDelegate connectionDelegate, EndpointConfig? endpointConfig, CancellationToken cancellationToken)
{
if (_transportFactory is null)
if (_transportFactories.Count == 0)
{
throw new InvalidOperationException($"Cannot bind with {nameof(ConnectionDelegate)} no {nameof(IConnectionListenerFactory)} is registered.");
}

var transport = await _transportFactory.BindAsync(endPoint, cancellationToken).ConfigureAwait(false);
StartAcceptLoop(new GenericConnectionListener(transport), c => connectionDelegate(c), endpointConfig);
return transport.EndPoint;
foreach (var transportFactory in _transportFactories)
{
var selector = transportFactory as IConnectionListenerFactorySelector;
if (CanBindFactory(endPoint, selector))
{
var transport = await transportFactory.BindAsync(endPoint, cancellationToken).ConfigureAwait(false);
StartAcceptLoop(new GenericConnectionListener(transport), c => connectionDelegate(c), endpointConfig);
return transport.EndPoint;
}
}

throw new InvalidOperationException($"No registered {nameof(IConnectionListenerFactory)} supports endpoint {endPoint.GetType().Name}: {endPoint}");
}

public async Task<EndPoint> BindAsync(EndPoint endPoint, MultiplexedConnectionDelegate multiplexedConnectionDelegate, ListenOptions listenOptions, CancellationToken cancellationToken)
{
if (_multiplexedTransportFactory is null)
if (_multiplexedTransportFactories.Count == 0)
{
throw new InvalidOperationException($"Cannot bind with {nameof(MultiplexedConnectionDelegate)} no {nameof(IMultiplexedConnectionListenerFactory)} is registered.");
}
Expand Down Expand Up @@ -87,9 +96,25 @@ public async Task<EndPoint> BindAsync(EndPoint endPoint, MultiplexedConnectionDe
});
}

var transport = await _multiplexedTransportFactory.BindAsync(endPoint, features, cancellationToken).ConfigureAwait(false);
StartAcceptLoop(new GenericMultiplexedConnectionListener(transport), c => multiplexedConnectionDelegate(c), listenOptions.EndpointConfig);
return transport.EndPoint;
foreach (var multiplexedTransportFactory in _multiplexedTransportFactories)
{
var selector = multiplexedTransportFactory as IConnectionListenerFactorySelector;
if (CanBindFactory(endPoint, selector))
{
var transport = await multiplexedTransportFactory.BindAsync(endPoint, features, cancellationToken).ConfigureAwait(false);
StartAcceptLoop(new GenericMultiplexedConnectionListener(transport), c => multiplexedConnectionDelegate(c), listenOptions.EndpointConfig);
return transport.EndPoint;
}
}

throw new InvalidOperationException($"No registered {nameof(IMultiplexedConnectionListenerFactory)} supports endpoint {endPoint.GetType().Name}: {endPoint}");
}

private static bool CanBindFactory(EndPoint endPoint, IConnectionListenerFactorySelector? selector)
{
// By default, the last registered factory binds to the endpoint.
Copy link
Member

@Tratcher Tratcher Nov 1, 2022

Choose a reason for hiding this comment

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

How do you ensure the last wins? Are you relying on the order of items in the DI injected Lists? I thought those lists where given in the order added, so you'd have to reverse it to get the last one first?

edit Nevermind, I found the Reverse call in KestrelServerImpl.

// A factory can implement IConnectionListenerFactorySelector to decide whether it can bind to the endpoint.
return selector?.CanBind(endPoint) ?? true;
}

/// <summary>
Expand Down
26 changes: 13 additions & 13 deletions src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ internal sealed class KestrelServerImpl : IServer
{
private readonly ServerAddressesFeature _serverAddresses;
private readonly TransportManager _transportManager;
private readonly IConnectionListenerFactory? _transportFactory;
private readonly IMultiplexedConnectionListenerFactory? _multiplexedTransportFactory;
private readonly List<IConnectionListenerFactory> _transportFactories;
private readonly List<IMultiplexedConnectionListenerFactory> _multiplexedTransportFactories;

private readonly SemaphoreSlim _bindSemaphore = new SemaphoreSlim(initialCount: 1);
private bool _hasStarted;
Expand All @@ -37,7 +37,7 @@ public KestrelServerImpl(
IOptions<KestrelServerOptions> options,
IEnumerable<IConnectionListenerFactory> transportFactories,
ILoggerFactory loggerFactory)
: this(transportFactories, null, CreateServiceContext(options, loggerFactory, null))
: this(transportFactories, Array.Empty<IMultiplexedConnectionListenerFactory>(), CreateServiceContext(options, loggerFactory, null))
{
}

Expand All @@ -62,22 +62,22 @@ public KestrelServerImpl(

// For testing
internal KestrelServerImpl(IConnectionListenerFactory transportFactory, ServiceContext serviceContext)
: this(new[] { transportFactory }, null, serviceContext)
: this(new[] { transportFactory }, Array.Empty<IMultiplexedConnectionListenerFactory>(), serviceContext)
{
}

// For testing
internal KestrelServerImpl(
IEnumerable<IConnectionListenerFactory> transportFactories,
IEnumerable<IMultiplexedConnectionListenerFactory>? multiplexedFactories,
IEnumerable<IMultiplexedConnectionListenerFactory> multiplexedFactories,
ServiceContext serviceContext)
{
ArgumentNullException.ThrowIfNull(transportFactories);

_transportFactory = transportFactories.LastOrDefault();
_multiplexedTransportFactory = multiplexedFactories?.LastOrDefault();
_transportFactories = transportFactories.Reverse().ToList();
_multiplexedTransportFactories = multiplexedFactories.Reverse().ToList();

if (_transportFactory == null && _multiplexedTransportFactory == null)
if (_transportFactories.Count == 0 && _multiplexedTransportFactories.Count == 0)
{
throw new InvalidOperationException(CoreStrings.TransportNotFound);
}
Expand All @@ -88,7 +88,7 @@ internal KestrelServerImpl(
_serverAddresses = new ServerAddressesFeature();
Features.Set<IServerAddressesFeature>(_serverAddresses);

_transportManager = new TransportManager(_transportFactory, _multiplexedTransportFactory, ServiceContext);
_transportManager = new TransportManager(_transportFactories, _multiplexedTransportFactories, ServiceContext);

HttpCharacters.Initialize();
}
Expand Down Expand Up @@ -177,14 +177,14 @@ async Task OnBind(ListenOptions options, CancellationToken onBindCancellationTok
}

// Quic isn't registered if it's not supported, throw if we can't fall back to 1 or 2
if (hasHttp3 && _multiplexedTransportFactory is null && !(hasHttp1 || hasHttp2))
if (hasHttp3 && _multiplexedTransportFactories.Count == 0 && !(hasHttp1 || hasHttp2))
{
throw new InvalidOperationException("This platform doesn't support QUIC or HTTP/3.");
}

// Disable adding alt-svc header if endpoint has configured not to or there is no
// multiplexed transport factory, which happens if QUIC isn't supported.
var addAltSvcHeader = !options.DisableAltSvcHeader && _multiplexedTransportFactory != null;
var addAltSvcHeader = !options.DisableAltSvcHeader && _multiplexedTransportFactories.Count > 0;

var configuredEndpoint = options.EndPoint;

Expand All @@ -193,7 +193,7 @@ async Task OnBind(ListenOptions options, CancellationToken onBindCancellationTok
|| options.Protocols == HttpProtocols.None) // TODO a test fails because it doesn't throw an exception in the right place
// when there is no HttpProtocols in KestrelServer, can we remove/change the test?
{
if (_transportFactory is null)
if (_transportFactories.Count == 0)
{
throw new InvalidOperationException($"Cannot start HTTP/1.x or HTTP/2 server if no {nameof(IConnectionListenerFactory)} is registered.");
}
Expand All @@ -207,7 +207,7 @@ async Task OnBind(ListenOptions options, CancellationToken onBindCancellationTok
options.EndPoint = await _transportManager.BindAsync(configuredEndpoint, connectionDelegate, options.EndpointConfig, onBindCancellationToken).ConfigureAwait(false);
}

if (hasHttp3 && _multiplexedTransportFactory is not null)
if (hasHttp3 && _multiplexedTransportFactories.Count > 0)
{
// Check if a previous transport has changed the endpoint. If it has then the endpoint is dynamic and we can't guarantee it will work for other transports.
// For more details, see https://github.com/dotnet/aspnetcore/issues/42982
Expand Down
104 changes: 104 additions & 0 deletions src/Servers/Kestrel/Core/test/KestrelServerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,90 @@ public void StartWithMultipleTransportFactoriesDoesNotThrow()
StartDummyApplication(server);
}

[Fact]
public async Task StartWithNoValidTransportFactoryThrows()
{
var serverOptions = CreateServerOptions();
serverOptions.Listen(new IPEndPoint(IPAddress.Loopback, 0));

var server = new KestrelServerImpl(
Options.Create<KestrelServerOptions>(serverOptions),
new List<IConnectionListenerFactory> { new NonBindableTransportFactory() },
new LoggerFactory(new[] { new KestrelTestLoggerProvider() }));

var exception = await Assert.ThrowsAsync<InvalidOperationException>(
async () => await server.StartAsync(new DummyApplication(context => Task.CompletedTask), CancellationToken.None));

Assert.Equal("No registered IConnectionListenerFactory supports endpoint IPEndPoint: 127.0.0.1:0", exception.Message);
}

[Fact]
public async Task StartWithMultipleTransportFactories_UseSupported()
{
var endpoint = new IPEndPoint(IPAddress.Loopback, 0);
var serverOptions = CreateServerOptions();
serverOptions.Listen(endpoint);

var transportFactory = new MockTransportFactory();

var server = new KestrelServerImpl(
Options.Create<KestrelServerOptions>(serverOptions),
new List<IConnectionListenerFactory> { transportFactory, new NonBindableTransportFactory() },
new LoggerFactory(new[] { new KestrelTestLoggerProvider() }));

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

Assert.Collection(transportFactory.BoundEndPoints,
ep => Assert.Equal(endpoint, ep.OriginalEndPoint));
}

[Fact]
public async Task StartWithNoValidTransportFactoryThrows_Http3()
{
var serverOptions = CreateServerOptions();
serverOptions.Listen(new IPEndPoint(IPAddress.Loopback, 0), c =>
{
c.Protocols = HttpProtocols.Http3;
c.UseHttps(TestResources.GetTestCertificate());
});

var server = new KestrelServerImpl(
Options.Create<KestrelServerOptions>(serverOptions),
new List<IConnectionListenerFactory>(),
new List<IMultiplexedConnectionListenerFactory> { new NonBindableMultiplexedTransportFactory() },
new LoggerFactory(new[] { new KestrelTestLoggerProvider() }));

var exception = await Assert.ThrowsAsync<InvalidOperationException>(
async () => await server.StartAsync(new DummyApplication(context => Task.CompletedTask), CancellationToken.None));

Assert.Equal("No registered IMultiplexedConnectionListenerFactory supports endpoint IPEndPoint: 127.0.0.1:0", exception.Message);
}

[Fact]
public async Task StartWithMultipleTransportFactories_Http3_UseSupported()
{
var endpoint = new IPEndPoint(IPAddress.Loopback, 0);
var serverOptions = CreateServerOptions();
serverOptions.Listen(endpoint, c =>
{
c.Protocols = HttpProtocols.Http3;
c.UseHttps(TestResources.GetTestCertificate());
});

var transportFactory = new MockMultiplexedTransportFactory();

var server = new KestrelServerImpl(
Options.Create<KestrelServerOptions>(serverOptions),
new List<IConnectionListenerFactory>(),
new List<IMultiplexedConnectionListenerFactory> { transportFactory, new NonBindableMultiplexedTransportFactory() },
new LoggerFactory(new[] { new KestrelTestLoggerProvider() }));

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

Assert.Collection(transportFactory.BoundEndPoints,
ep => Assert.Equal(endpoint, ep.OriginalEndPoint));
}

[Fact]
public async Task ListenWithCustomEndpoint_DoesNotThrow()
{
Expand Down Expand Up @@ -850,6 +934,26 @@ public ValueTask<IConnectionListener> BindAsync(EndPoint endpoint, CancellationT
}
}

private class NonBindableTransportFactory : IConnectionListenerFactory, IConnectionListenerFactorySelector
{
public ValueTask<IConnectionListener> BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default)
{
throw new InvalidOperationException();
}

public bool CanBind(EndPoint endpoint) => false;
}

private class NonBindableMultiplexedTransportFactory : IMultiplexedConnectionListenerFactory, IConnectionListenerFactorySelector
{
public ValueTask<IMultiplexedConnectionListener> BindAsync(EndPoint endpoint, IFeatureCollection features = null, CancellationToken cancellationToken = default)
{
throw new InvalidOperationException();
}

public bool CanBind(EndPoint endpoint) => false;
}

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