Skip to content

Commit 9c2b65a

Browse files
authored
HTTP/3: QUIC connection listener fixes (#35258)
1 parent 1c9f1d2 commit 9c2b65a

File tree

6 files changed

+188
-15
lines changed

6 files changed

+188
-15
lines changed

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

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
using System.Threading.Tasks;
1010
using Microsoft.AspNetCore.Builder;
1111
using Microsoft.AspNetCore.Connections;
12+
using Microsoft.AspNetCore.Hosting;
1213
using Microsoft.AspNetCore.Hosting.Server;
1314
using Microsoft.AspNetCore.Hosting.Server.Features;
15+
using Microsoft.AspNetCore.Http.Features;
1416
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
1517
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
1618
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
@@ -247,6 +249,78 @@ public void StartWithMultipleTransportFactoriesDoesNotThrow()
247249
StartDummyApplication(server);
248250
}
249251

252+
[Fact]
253+
public async Task ListenIPWithStaticPort_TransportsGetIPv6Any()
254+
{
255+
var options = new KestrelServerOptions();
256+
options.ApplicationServices = new ServiceCollection()
257+
.AddLogging()
258+
.BuildServiceProvider();
259+
options.ListenAnyIP(5000, options =>
260+
{
261+
options.UseHttps(TestResources.GetTestCertificate());
262+
options.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
263+
});
264+
265+
var mockTransportFactory = new MockTransportFactory();
266+
var mockMultiplexedTransportFactory = new MockMultiplexedTransportFactory();
267+
268+
using var server = new KestrelServerImpl(
269+
Options.Create(options),
270+
new List<IConnectionListenerFactory>() { mockTransportFactory },
271+
new List<IMultiplexedConnectionListenerFactory>() { mockMultiplexedTransportFactory },
272+
new LoggerFactory(new[] { new KestrelTestLoggerProvider() }));
273+
274+
await server.StartAsync(new DummyApplication(context => Task.CompletedTask), CancellationToken.None);
275+
276+
var transportEndPoint = Assert.Single(mockTransportFactory.BoundEndPoints);
277+
var multiplexedTransportEndPoint = Assert.Single(mockMultiplexedTransportFactory.BoundEndPoints);
278+
279+
// Both transports should get the IPv6Any
280+
Assert.Equal(IPAddress.IPv6Any, ((IPEndPoint)transportEndPoint.OriginalEndPoint).Address);
281+
Assert.Equal(IPAddress.IPv6Any, ((IPEndPoint)multiplexedTransportEndPoint.OriginalEndPoint).Address);
282+
283+
Assert.Equal(5000, ((IPEndPoint)transportEndPoint.OriginalEndPoint).Port);
284+
Assert.Equal(5000, ((IPEndPoint)multiplexedTransportEndPoint.OriginalEndPoint).Port);
285+
}
286+
287+
[Fact]
288+
public async Task ListenIPWithEphemeralPort_TransportsGetIPv6Any()
289+
{
290+
var options = new KestrelServerOptions();
291+
options.ApplicationServices = new ServiceCollection()
292+
.AddLogging()
293+
.BuildServiceProvider();
294+
options.ListenAnyIP(0, options =>
295+
{
296+
options.UseHttps(TestResources.GetTestCertificate());
297+
options.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
298+
});
299+
300+
var mockTransportFactory = new MockTransportFactory();
301+
var mockMultiplexedTransportFactory = new MockMultiplexedTransportFactory();
302+
303+
using var server = new KestrelServerImpl(
304+
Options.Create(options),
305+
new List<IConnectionListenerFactory>() { mockTransportFactory },
306+
new List<IMultiplexedConnectionListenerFactory>() { mockMultiplexedTransportFactory },
307+
new LoggerFactory(new[] { new KestrelTestLoggerProvider() }));
308+
309+
await server.StartAsync(new DummyApplication(context => Task.CompletedTask), CancellationToken.None);
310+
311+
var transportEndPoint = Assert.Single(mockTransportFactory.BoundEndPoints);
312+
var multiplexedTransportEndPoint = Assert.Single(mockMultiplexedTransportFactory.BoundEndPoints);
313+
314+
Assert.Equal(IPAddress.IPv6Any, ((IPEndPoint)transportEndPoint.OriginalEndPoint).Address);
315+
Assert.Equal(IPAddress.IPv6Any, ((IPEndPoint)multiplexedTransportEndPoint.OriginalEndPoint).Address);
316+
317+
// Should have been assigned a random value.
318+
Assert.NotEqual(0, ((IPEndPoint)transportEndPoint.BoundEndPoint).Port);
319+
320+
// Same random value should be used for both transports.
321+
Assert.Equal(((IPEndPoint)transportEndPoint.BoundEndPoint).Port, ((IPEndPoint)multiplexedTransportEndPoint.BoundEndPoint).Port);
322+
}
323+
250324
[Fact]
251325
public async Task StopAsyncCallsCompleteWhenFirstCallCompletes()
252326
{
@@ -692,10 +766,24 @@ private static void StartDummyApplication(IServer server)
692766

693767
private class MockTransportFactory : IConnectionListenerFactory
694768
{
769+
public List<BindDetail> BoundEndPoints { get; } = new List<BindDetail>();
770+
695771
public ValueTask<IConnectionListener> BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default)
696772
{
773+
EndPoint resolvedEndPoint = endpoint;
774+
if (resolvedEndPoint is IPEndPoint ipEndPoint)
775+
{
776+
var port = ipEndPoint.Port == 0
777+
? Random.Shared.Next(IPEndPoint.MinPort, IPEndPoint.MaxPort)
778+
: ipEndPoint.Port;
779+
780+
resolvedEndPoint = new IPEndPoint(new IPAddress(ipEndPoint.Address.GetAddressBytes()), port);
781+
}
782+
783+
BoundEndPoints.Add(new BindDetail(endpoint, resolvedEndPoint));
784+
697785
var mock = new Mock<IConnectionListener>();
698-
mock.Setup(m => m.EndPoint).Returns(endpoint);
786+
mock.Setup(m => m.EndPoint).Returns(resolvedEndPoint);
699787
return new ValueTask<IConnectionListener>(mock.Object);
700788
}
701789
}
@@ -707,5 +795,31 @@ public ValueTask<IConnectionListener> BindAsync(EndPoint endpoint, CancellationT
707795
throw new InvalidOperationException();
708796
}
709797
}
798+
799+
private class MockMultiplexedTransportFactory : IMultiplexedConnectionListenerFactory
800+
{
801+
public List<BindDetail> BoundEndPoints { get; } = new List<BindDetail>();
802+
803+
public ValueTask<IMultiplexedConnectionListener> BindAsync(EndPoint endpoint, IFeatureCollection features = null, CancellationToken cancellationToken = default)
804+
{
805+
EndPoint resolvedEndPoint = endpoint;
806+
if (resolvedEndPoint is IPEndPoint ipEndPoint)
807+
{
808+
var port = ipEndPoint.Port == 0
809+
? Random.Shared.Next(IPEndPoint.MinPort, IPEndPoint.MaxPort)
810+
: ipEndPoint.Port;
811+
812+
resolvedEndPoint = new IPEndPoint(new IPAddress(ipEndPoint.Address.GetAddressBytes()), port);
813+
}
814+
815+
BoundEndPoints.Add(new BindDetail(endpoint, resolvedEndPoint));
816+
817+
var mock = new Mock<IMultiplexedConnectionListener>();
818+
mock.Setup(m => m.EndPoint).Returns(resolvedEndPoint);
819+
return new ValueTask<IMultiplexedConnectionListener>(mock.Object);
820+
}
821+
}
822+
823+
private record BindDetail(EndPoint OriginalEndPoint, EndPoint BoundEndPoint);
710824
}
711825
}

src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,26 @@ public QuicConnectionListener(QuicTransportOptions options, IQuicTrace log, EndP
3535
_context = new QuicTransportContext(_log, options);
3636
var quicListenerOptions = new QuicListenerOptions();
3737

38+
var listenEndPoint = endpoint as IPEndPoint;
39+
40+
if (listenEndPoint == null)
41+
{
42+
throw new InvalidOperationException($"QUIC doesn't support listening on the configured endpoint type. Expected {nameof(IPEndPoint)} but got {endpoint.GetType().Name}.");
43+
}
44+
45+
// Workaround for issue in System.Net.Quic
46+
// https://github.com/dotnet/runtime/issues/57241
47+
if (listenEndPoint.Address.Equals(IPAddress.Any) && listenEndPoint.Address != IPAddress.Any)
48+
{
49+
listenEndPoint = new IPEndPoint(IPAddress.Any, listenEndPoint.Port);
50+
}
51+
if (listenEndPoint.Address.Equals(IPAddress.IPv6Any) && listenEndPoint.Address != IPAddress.IPv6Any)
52+
{
53+
listenEndPoint = new IPEndPoint(IPAddress.IPv6Any, listenEndPoint.Port);
54+
}
55+
3856
quicListenerOptions.ServerAuthenticationOptions = sslServerAuthenticationOptions;
39-
quicListenerOptions.ListenEndPoint = endpoint as IPEndPoint;
57+
quicListenerOptions.ListenEndPoint = listenEndPoint;
4058
quicListenerOptions.IdleTimeout = options.IdleTimeout;
4159
quicListenerOptions.MaxBidirectionalStreams = options.MaxBidirectionalStreamCount;
4260
quicListenerOptions.MaxUnidirectionalStreams = options.MaxUnidirectionalStreamCount;

src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ public QuicTransportFactory(ILoggerFactory loggerFactory, IOptions<QuicTransport
4848
/// <returns>A </returns>
4949
public ValueTask<IMultiplexedConnectionListener> BindAsync(EndPoint endpoint, IFeatureCollection? features = null, CancellationToken cancellationToken = default)
5050
{
51+
if (endpoint == null)
52+
{
53+
throw new ArgumentNullException(nameof(endpoint));
54+
}
55+
5156
var sslServerAuthenticationOptions = features?.Get<SslServerAuthenticationOptions>();
5257

5358
if (sslServerAuthenticationOptions == null)

src/Servers/Kestrel/Transport.Quic/test/QuicTransportFactoryTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public async Task BindAsync_NoServerCertificate_Error()
4949
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => quicTransportFactory.BindAsync(new IPEndPoint(0, 0), features: features, cancellationToken: CancellationToken.None).AsTask()).DefaultTimeout();
5050

5151
// Assert
52-
Assert.Equal("SslServerAuthenticationOptions.ServerCertificate must be configured with a value.", ex.Message);
52+
Assert.Equal("SslServerAuthenticationOptions must provide a server certificate using ServerCertificate, ServerCertificateContext, or ServerCertificateSelectionCallback.", ex.Message);
5353
}
5454
}
5555
}

src/Servers/Kestrel/Transport.Quic/test/WebHostTests.cs

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,12 @@ public async Task Listen_Http3AndSocketsCoexistOnDifferentEndpoints_ClientSucces
8282
o.Listen(IPAddress.Parse("127.0.0.1"), http3Port, listenOptions =>
8383
{
8484
listenOptions.Protocols = Core.HttpProtocols.Http3;
85-
listenOptions.UseHttps();
85+
listenOptions.UseHttps(TestResources.GetTestCertificate());
8686
});
8787
o.Listen(IPAddress.Parse("127.0.0.1"), http1Port, listenOptions =>
8888
{
8989
listenOptions.Protocols = Core.HttpProtocols.Http1;
90-
listenOptions.UseHttps();
90+
listenOptions.UseHttps(TestResources.GetTestCertificate());
9191
});
9292
})
9393
.Configure(app =>
@@ -122,7 +122,7 @@ public async Task Listen_Http3AndSocketsCoexistOnSameEndpoint_ClientSuccess()
122122
o.Listen(IPAddress.Parse("127.0.0.1"), 5005, listenOptions =>
123123
{
124124
listenOptions.Protocols = Core.HttpProtocols.Http1AndHttp2AndHttp3;
125-
listenOptions.UseHttps();
125+
listenOptions.UseHttps(TestResources.GetTestCertificate());
126126
});
127127
})
128128
.Configure(app =>
@@ -157,7 +157,7 @@ public async Task Listen_Http3AndSocketsCoexistOnSameEndpoint_AltSvcEnabled_Upgr
157157
o.Listen(IPAddress.Parse("127.0.0.1"), 0, listenOptions =>
158158
{
159159
listenOptions.Protocols = Core.HttpProtocols.Http1AndHttp2AndHttp3;
160-
listenOptions.UseHttps();
160+
listenOptions.UseHttps(TestResources.GetTestCertificate());
161161
});
162162
})
163163
.Configure(app =>
@@ -225,7 +225,7 @@ public async Task Listen_Http3AndSocketsCoexistOnSameEndpoint_AltSvcDisabled_NoU
225225
o.Listen(IPAddress.Parse("127.0.0.1"), 0, listenOptions =>
226226
{
227227
listenOptions.Protocols = Core.HttpProtocols.Http1AndHttp2AndHttp3;
228-
listenOptions.UseHttps();
228+
listenOptions.UseHttps(TestResources.GetTestCertificate());
229229
});
230230
})
231231
.Configure(app =>
@@ -305,6 +305,42 @@ private static async Task CallHttp3AndHttp1EndpointsAsync(int http3Port, int htt
305305
}
306306
}
307307

308+
[ConditionalFact]
309+
[MsQuicSupported]
310+
public async Task StartAsync_Http3WithNonIPListener_ThrowError()
311+
{
312+
// Arrange
313+
var builder = new HostBuilder()
314+
.ConfigureWebHost(webHostBuilder =>
315+
{
316+
webHostBuilder
317+
.UseKestrel(o =>
318+
{
319+
o.ListenUnixSocket("/test-path", listenOptions =>
320+
{
321+
listenOptions.Protocols = Core.HttpProtocols.Http3;
322+
listenOptions.UseHttps(TestResources.GetTestCertificate());
323+
});
324+
})
325+
.Configure(app =>
326+
{
327+
app.Run(async context =>
328+
{
329+
await context.Response.WriteAsync("hello, world");
330+
});
331+
});
332+
})
333+
.ConfigureServices(AddTestLogging);
334+
335+
using var host = builder.Build();
336+
337+
// Act
338+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => host.StartAsync()).DefaultTimeout();
339+
340+
// Assert
341+
Assert.Equal("QUIC doesn't support listening on the configured endpoint type. Expected IPEndPoint but got UnixDomainSocketEndPoint.", ex.Message);
342+
}
343+
308344
private static HttpClient CreateClient()
309345
{
310346
var httpHandler = new HttpClientHandler();

src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ public async Task POST_ServerCompletesWithoutReadingRequestBody_ClientGetsRespon
277277
using (var host = builder.Build())
278278
using (var client = Http3Helpers.CreateClient())
279279
{
280-
await host.StartAsync();
280+
await host.StartAsync().DefaultTimeout();
281281

282282
var requestContent = new StreamingHttpContext();
283283

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

292-
var requestStream = await requestContent.GetStreamAsync();
292+
var requestStream = await requestContent.GetStreamAsync().DefaultTimeout();
293293

294294
// Send headers
295-
await requestStream.FlushAsync();
295+
await requestStream.FlushAsync().DefaultTimeout();
296296
// Write content
297-
await requestStream.WriteAsync(TestData);
297+
await requestStream.WriteAsync(TestData).DefaultTimeout();
298298

299-
var response = await responseTask;
299+
var response = await responseTask.DefaultTimeout();
300300

301301
// Assert
302302
response.EnsureSuccessStatusCode();
303303
Assert.Equal(HttpVersion.Version30, response.Version);
304-
var responseText = await response.Content.ReadAsStringAsync();
304+
var responseText = await response.Content.ReadAsStringAsync().DefaultTimeout();
305305
Assert.Equal("Hello world", responseText);
306306

307-
await host.StopAsync();
307+
await host.StopAsync().DefaultTimeout();
308308
}
309309
}
310310

0 commit comments

Comments
 (0)