Skip to content

Commit c8d252d

Browse files
authored
Support side-by-side transports in Kestrel (#44657)
1 parent abf67cb commit c8d252d

File tree

8 files changed

+187
-27
lines changed

8 files changed

+187
-27
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Net;
5+
6+
namespace Microsoft.AspNetCore.Connections;
7+
8+
/// <summary>
9+
/// Defines an interface that determines whether the listener factory supports binding to the specified <see cref="EndPoint"/>.
10+
/// </summary>
11+
/// <remarks>
12+
/// This interface should be implemented by <see cref="IConnectionListenerFactory"/> and <see cref="IMultiplexedConnectionListenerFactory"/>
13+
/// types that want to control want endpoint instances they can bind to.
14+
/// </remarks>
15+
public interface IConnectionListenerFactorySelector
16+
{
17+
/// <summary>
18+
/// Returns a value that indicates whether the listener factory supports binding to the specified <see cref="EndPoint"/>.
19+
/// </summary>
20+
/// <param name="endpoint">The <see cref="EndPoint" /> to bind to.</param>
21+
/// <returns>A value that indicates whether the listener factory supports binding to the specified <see cref="EndPoint"/>.</returns>
22+
bool CanBind(EndPoint endpoint);
23+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
#nullable enable
22
Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature
33
Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature.OnClosed(System.Action<object?>! callback, object? state) -> void
4+
Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector
5+
Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector.CanBind(System.Net.EndPoint! endpoint) -> bool

src/Servers/Connections.Abstractions/src/PublicAPI/net7.0/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#nullable enable
22
Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature
33
Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature.OnClosed(System.Action<object?>! callback, object? state) -> void
4+
Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector
5+
Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector.CanBind(System.Net.EndPoint! endpoint) -> bool
46
Microsoft.AspNetCore.Connections.TlsConnectionCallbackContext
57
Microsoft.AspNetCore.Connections.TlsConnectionCallbackContext.ClientHelloInfo.get -> System.Net.Security.SslClientHelloInfo
68
Microsoft.AspNetCore.Connections.TlsConnectionCallbackContext.ClientHelloInfo.set -> void
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
#nullable enable
22
Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature
33
Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature.OnClosed(System.Action<object?>! callback, object? state) -> void
4+
Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector
5+
Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector.CanBind(System.Net.EndPoint! endpoint) -> bool
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
#nullable enable
22
Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature
33
Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature.OnClosed(System.Action<object?>! callback, object? state) -> void
4+
Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector
5+
Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector.CanBind(System.Net.EndPoint! endpoint) -> bool

src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,17 @@ internal sealed class TransportManager
1818
{
1919
private readonly List<ActiveTransport> _transports = new List<ActiveTransport>();
2020

21-
private readonly IConnectionListenerFactory? _transportFactory;
22-
private readonly IMultiplexedConnectionListenerFactory? _multiplexedTransportFactory;
21+
private readonly List<IConnectionListenerFactory> _transportFactories;
22+
private readonly List<IMultiplexedConnectionListenerFactory> _multiplexedTransportFactories;
2323
private readonly ServiceContext _serviceContext;
2424

2525
public TransportManager(
26-
IConnectionListenerFactory? transportFactory,
27-
IMultiplexedConnectionListenerFactory? multiplexedTransportFactory,
26+
List<IConnectionListenerFactory> transportFactories,
27+
List<IMultiplexedConnectionListenerFactory> multiplexedTransportFactories,
2828
ServiceContext serviceContext)
2929
{
30-
_transportFactory = transportFactory;
31-
_multiplexedTransportFactory = multiplexedTransportFactory;
30+
_transportFactories = transportFactories;
31+
_multiplexedTransportFactories = multiplexedTransportFactories;
3232
_serviceContext = serviceContext;
3333
}
3434

@@ -37,19 +37,28 @@ public TransportManager(
3737

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

45-
var transport = await _transportFactory.BindAsync(endPoint, cancellationToken).ConfigureAwait(false);
46-
StartAcceptLoop(new GenericConnectionListener(transport), c => connectionDelegate(c), endpointConfig);
47-
return transport.EndPoint;
45+
foreach (var transportFactory in _transportFactories)
46+
{
47+
var selector = transportFactory as IConnectionListenerFactorySelector;
48+
if (CanBindFactory(endPoint, selector))
49+
{
50+
var transport = await transportFactory.BindAsync(endPoint, cancellationToken).ConfigureAwait(false);
51+
StartAcceptLoop(new GenericConnectionListener(transport), c => connectionDelegate(c), endpointConfig);
52+
return transport.EndPoint;
53+
}
54+
}
55+
56+
throw new InvalidOperationException($"No registered {nameof(IConnectionListenerFactory)} supports endpoint {endPoint.GetType().Name}: {endPoint}");
4857
}
4958

5059
public async Task<EndPoint> BindAsync(EndPoint endPoint, MultiplexedConnectionDelegate multiplexedConnectionDelegate, ListenOptions listenOptions, CancellationToken cancellationToken)
5160
{
52-
if (_multiplexedTransportFactory is null)
61+
if (_multiplexedTransportFactories.Count == 0)
5362
{
5463
throw new InvalidOperationException($"Cannot bind with {nameof(MultiplexedConnectionDelegate)} no {nameof(IMultiplexedConnectionListenerFactory)} is registered.");
5564
}
@@ -87,9 +96,25 @@ public async Task<EndPoint> BindAsync(EndPoint endPoint, MultiplexedConnectionDe
8796
});
8897
}
8998

90-
var transport = await _multiplexedTransportFactory.BindAsync(endPoint, features, cancellationToken).ConfigureAwait(false);
91-
StartAcceptLoop(new GenericMultiplexedConnectionListener(transport), c => multiplexedConnectionDelegate(c), listenOptions.EndpointConfig);
92-
return transport.EndPoint;
99+
foreach (var multiplexedTransportFactory in _multiplexedTransportFactories)
100+
{
101+
var selector = multiplexedTransportFactory as IConnectionListenerFactorySelector;
102+
if (CanBindFactory(endPoint, selector))
103+
{
104+
var transport = await multiplexedTransportFactory.BindAsync(endPoint, features, cancellationToken).ConfigureAwait(false);
105+
StartAcceptLoop(new GenericMultiplexedConnectionListener(transport), c => multiplexedConnectionDelegate(c), listenOptions.EndpointConfig);
106+
return transport.EndPoint;
107+
}
108+
}
109+
110+
throw new InvalidOperationException($"No registered {nameof(IMultiplexedConnectionListenerFactory)} supports endpoint {endPoint.GetType().Name}: {endPoint}");
111+
}
112+
113+
private static bool CanBindFactory(EndPoint endPoint, IConnectionListenerFactorySelector? selector)
114+
{
115+
// By default, the last registered factory binds to the endpoint.
116+
// A factory can implement IConnectionListenerFactorySelector to decide whether it can bind to the endpoint.
117+
return selector?.CanBind(endPoint) ?? true;
93118
}
94119

95120
/// <summary>

src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ internal sealed class KestrelServerImpl : IServer
2222
{
2323
private readonly ServerAddressesFeature _serverAddresses;
2424
private readonly TransportManager _transportManager;
25-
private readonly IConnectionListenerFactory? _transportFactory;
26-
private readonly IMultiplexedConnectionListenerFactory? _multiplexedTransportFactory;
25+
private readonly List<IConnectionListenerFactory> _transportFactories;
26+
private readonly List<IMultiplexedConnectionListenerFactory> _multiplexedTransportFactories;
2727

2828
private readonly SemaphoreSlim _bindSemaphore = new SemaphoreSlim(initialCount: 1);
2929
private bool _hasStarted;
@@ -37,7 +37,7 @@ public KestrelServerImpl(
3737
IOptions<KestrelServerOptions> options,
3838
IEnumerable<IConnectionListenerFactory> transportFactories,
3939
ILoggerFactory loggerFactory)
40-
: this(transportFactories, null, CreateServiceContext(options, loggerFactory, null))
40+
: this(transportFactories, Array.Empty<IMultiplexedConnectionListenerFactory>(), CreateServiceContext(options, loggerFactory, null))
4141
{
4242
}
4343

@@ -62,22 +62,22 @@ public KestrelServerImpl(
6262

6363
// For testing
6464
internal KestrelServerImpl(IConnectionListenerFactory transportFactory, ServiceContext serviceContext)
65-
: this(new[] { transportFactory }, null, serviceContext)
65+
: this(new[] { transportFactory }, Array.Empty<IMultiplexedConnectionListenerFactory>(), serviceContext)
6666
{
6767
}
6868

6969
// For testing
7070
internal KestrelServerImpl(
7171
IEnumerable<IConnectionListenerFactory> transportFactories,
72-
IEnumerable<IMultiplexedConnectionListenerFactory>? multiplexedFactories,
72+
IEnumerable<IMultiplexedConnectionListenerFactory> multiplexedFactories,
7373
ServiceContext serviceContext)
7474
{
7575
ArgumentNullException.ThrowIfNull(transportFactories);
7676

77-
_transportFactory = transportFactories.LastOrDefault();
78-
_multiplexedTransportFactory = multiplexedFactories?.LastOrDefault();
77+
_transportFactories = transportFactories.Reverse().ToList();
78+
_multiplexedTransportFactories = multiplexedFactories.Reverse().ToList();
7979

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

91-
_transportManager = new TransportManager(_transportFactory, _multiplexedTransportFactory, ServiceContext);
91+
_transportManager = new TransportManager(_transportFactories, _multiplexedTransportFactories, ServiceContext);
9292

9393
HttpCharacters.Initialize();
9494
}
@@ -177,14 +177,14 @@ async Task OnBind(ListenOptions options, CancellationToken onBindCancellationTok
177177
}
178178

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

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

189189
var configuredEndpoint = options.EndPoint;
190190

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

210-
if (hasHttp3 && _multiplexedTransportFactory is not null)
210+
if (hasHttp3 && _multiplexedTransportFactories.Count > 0)
211211
{
212212
// 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.
213213
// For more details, see https://github.com/dotnet/aspnetcore/issues/42982

src/Servers/Kestrel/Core/test/KestrelServerTests.cs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,90 @@ public void StartWithMultipleTransportFactoriesDoesNotThrow()
243243
StartDummyApplication(server);
244244
}
245245

246+
[Fact]
247+
public async Task StartWithNoValidTransportFactoryThrows()
248+
{
249+
var serverOptions = CreateServerOptions();
250+
serverOptions.Listen(new IPEndPoint(IPAddress.Loopback, 0));
251+
252+
var server = new KestrelServerImpl(
253+
Options.Create<KestrelServerOptions>(serverOptions),
254+
new List<IConnectionListenerFactory> { new NonBindableTransportFactory() },
255+
new LoggerFactory(new[] { new KestrelTestLoggerProvider() }));
256+
257+
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
258+
async () => await server.StartAsync(new DummyApplication(context => Task.CompletedTask), CancellationToken.None));
259+
260+
Assert.Equal("No registered IConnectionListenerFactory supports endpoint IPEndPoint: 127.0.0.1:0", exception.Message);
261+
}
262+
263+
[Fact]
264+
public async Task StartWithMultipleTransportFactories_UseSupported()
265+
{
266+
var endpoint = new IPEndPoint(IPAddress.Loopback, 0);
267+
var serverOptions = CreateServerOptions();
268+
serverOptions.Listen(endpoint);
269+
270+
var transportFactory = new MockTransportFactory();
271+
272+
var server = new KestrelServerImpl(
273+
Options.Create<KestrelServerOptions>(serverOptions),
274+
new List<IConnectionListenerFactory> { transportFactory, new NonBindableTransportFactory() },
275+
new LoggerFactory(new[] { new KestrelTestLoggerProvider() }));
276+
277+
await server.StartAsync(new DummyApplication(context => Task.CompletedTask), CancellationToken.None);
278+
279+
Assert.Collection(transportFactory.BoundEndPoints,
280+
ep => Assert.Equal(endpoint, ep.OriginalEndPoint));
281+
}
282+
283+
[Fact]
284+
public async Task StartWithNoValidTransportFactoryThrows_Http3()
285+
{
286+
var serverOptions = CreateServerOptions();
287+
serverOptions.Listen(new IPEndPoint(IPAddress.Loopback, 0), c =>
288+
{
289+
c.Protocols = HttpProtocols.Http3;
290+
c.UseHttps(TestResources.GetTestCertificate());
291+
});
292+
293+
var server = new KestrelServerImpl(
294+
Options.Create<KestrelServerOptions>(serverOptions),
295+
new List<IConnectionListenerFactory>(),
296+
new List<IMultiplexedConnectionListenerFactory> { new NonBindableMultiplexedTransportFactory() },
297+
new LoggerFactory(new[] { new KestrelTestLoggerProvider() }));
298+
299+
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
300+
async () => await server.StartAsync(new DummyApplication(context => Task.CompletedTask), CancellationToken.None));
301+
302+
Assert.Equal("No registered IMultiplexedConnectionListenerFactory supports endpoint IPEndPoint: 127.0.0.1:0", exception.Message);
303+
}
304+
305+
[Fact]
306+
public async Task StartWithMultipleTransportFactories_Http3_UseSupported()
307+
{
308+
var endpoint = new IPEndPoint(IPAddress.Loopback, 0);
309+
var serverOptions = CreateServerOptions();
310+
serverOptions.Listen(endpoint, c =>
311+
{
312+
c.Protocols = HttpProtocols.Http3;
313+
c.UseHttps(TestResources.GetTestCertificate());
314+
});
315+
316+
var transportFactory = new MockMultiplexedTransportFactory();
317+
318+
var server = new KestrelServerImpl(
319+
Options.Create<KestrelServerOptions>(serverOptions),
320+
new List<IConnectionListenerFactory>(),
321+
new List<IMultiplexedConnectionListenerFactory> { transportFactory, new NonBindableMultiplexedTransportFactory() },
322+
new LoggerFactory(new[] { new KestrelTestLoggerProvider() }));
323+
324+
await server.StartAsync(new DummyApplication(context => Task.CompletedTask), CancellationToken.None);
325+
326+
Assert.Collection(transportFactory.BoundEndPoints,
327+
ep => Assert.Equal(endpoint, ep.OriginalEndPoint));
328+
}
329+
246330
[Fact]
247331
public async Task ListenWithCustomEndpoint_DoesNotThrow()
248332
{
@@ -850,6 +934,26 @@ public ValueTask<IConnectionListener> BindAsync(EndPoint endpoint, CancellationT
850934
}
851935
}
852936

937+
private class NonBindableTransportFactory : IConnectionListenerFactory, IConnectionListenerFactorySelector
938+
{
939+
public ValueTask<IConnectionListener> BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default)
940+
{
941+
throw new InvalidOperationException();
942+
}
943+
944+
public bool CanBind(EndPoint endpoint) => false;
945+
}
946+
947+
private class NonBindableMultiplexedTransportFactory : IMultiplexedConnectionListenerFactory, IConnectionListenerFactorySelector
948+
{
949+
public ValueTask<IMultiplexedConnectionListener> BindAsync(EndPoint endpoint, IFeatureCollection features = null, CancellationToken cancellationToken = default)
950+
{
951+
throw new InvalidOperationException();
952+
}
953+
954+
public bool CanBind(EndPoint endpoint) => false;
955+
}
956+
853957
private class MockMultiplexedTransportFactory : IMultiplexedConnectionListenerFactory
854958
{
855959
public List<BindDetail> BoundEndPoints { get; } = new List<BindDetail>();

0 commit comments

Comments
 (0)