diff --git a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs index f7cd3faa0bf7bd2016100375dd8df95f49cdc72f..eb976cc4b0b22ac770b010388c74b49443a99598 100644 --- a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs +++ b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs @@ -9,8 +9,10 @@ using System.Threading; 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; @@ -247,6 +249,78 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests StartDummyApplication(server); } + [Fact] + public async Task ListenIPWithStaticPort_TransportsGetIPv6Any() + { + var options = new KestrelServerOptions(); + options.ApplicationServices = new ServiceCollection() + .AddLogging() + .BuildServiceProvider(); + options.ListenAnyIP(5000, options => + { + options.UseHttps(TestResources.GetTestCertificate()); + 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.Equal(IPAddress.IPv6Any, ((IPEndPoint)transportEndPoint.OriginalEndPoint).Address); + Assert.Equal(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(TestResources.GetTestCertificate()); + 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.Equal(IPAddress.IPv6Any, ((IPEndPoint)transportEndPoint.OriginalEndPoint).Address); + Assert.Equal(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() { @@ -692,10 +766,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests 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); } } @@ -707,5 +795,31 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests 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); } } diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs index 975aa8bf3946a0127374812fcb8e1108fcfb6ec0..9a9528bc04a9516abcef242b9446632196405cfb 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs @@ -35,8 +35,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal _context = new QuicTransportContext(_log, options); var quicListenerOptions = new QuicListenerOptions(); + var listenEndPoint = endpoint as IPEndPoint; + + if (listenEndPoint == null) + { + throw new InvalidOperationException($"QUIC doesn't support listening on the configured endpoint type. Expected {nameof(IPEndPoint)} but got {endpoint.GetType().Name}."); + } + + // Workaround for issue in System.Net.Quic + // https://github.com/dotnet/runtime/issues/57241 + if (listenEndPoint.Address.Equals(IPAddress.Any) && listenEndPoint.Address != IPAddress.Any) + { + listenEndPoint = new IPEndPoint(IPAddress.Any, listenEndPoint.Port); + } + if (listenEndPoint.Address.Equals(IPAddress.IPv6Any) && listenEndPoint.Address != IPAddress.IPv6Any) + { + listenEndPoint = new IPEndPoint(IPAddress.IPv6Any, listenEndPoint.Port); + } + quicListenerOptions.ServerAuthenticationOptions = sslServerAuthenticationOptions; - quicListenerOptions.ListenEndPoint = endpoint as IPEndPoint; + quicListenerOptions.ListenEndPoint = listenEndPoint; quicListenerOptions.IdleTimeout = options.IdleTimeout; quicListenerOptions.MaxBidirectionalStreams = options.MaxBidirectionalStreamCount; quicListenerOptions.MaxUnidirectionalStreams = options.MaxUnidirectionalStreamCount; diff --git a/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs b/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs index 8a82ae301f9b386d7fae83a41cf11960418a7987..4e20ff926b4d4b560defc8da40bf668c109c8e91 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs @@ -48,6 +48,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic /// <returns>A </returns> public ValueTask<IMultiplexedConnectionListener> BindAsync(EndPoint endpoint, IFeatureCollection? features = null, CancellationToken cancellationToken = default) { + if (endpoint == null) + { + throw new ArgumentNullException(nameof(endpoint)); + } + var sslServerAuthenticationOptions = features?.Get<SslServerAuthenticationOptions>(); if (sslServerAuthenticationOptions == null) diff --git a/src/Servers/Kestrel/Transport.Quic/test/QuicTransportFactoryTests.cs b/src/Servers/Kestrel/Transport.Quic/test/QuicTransportFactoryTests.cs index 730be811833b0db48968c0057e374aa88f0e86fb..da0760e742dd12f18c325f753f66e4d16ab2b27c 100644 --- a/src/Servers/Kestrel/Transport.Quic/test/QuicTransportFactoryTests.cs +++ b/src/Servers/Kestrel/Transport.Quic/test/QuicTransportFactoryTests.cs @@ -49,7 +49,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => quicTransportFactory.BindAsync(new IPEndPoint(0, 0), features: features, cancellationToken: CancellationToken.None).AsTask()).DefaultTimeout(); // Assert - Assert.Equal("SslServerAuthenticationOptions.ServerCertificate must be configured with a value.", ex.Message); + Assert.Equal("SslServerAuthenticationOptions must provide a server certificate using ServerCertificate, ServerCertificateContext, or ServerCertificateSelectionCallback.", ex.Message); } } } diff --git a/src/Servers/Kestrel/Transport.Quic/test/WebHostTests.cs b/src/Servers/Kestrel/Transport.Quic/test/WebHostTests.cs index 34b56980b5beaf3c84e010b943a413be8d7c490d..10b5d6d113da42d40d36eb56bcedbc047fe35936 100644 --- a/src/Servers/Kestrel/Transport.Quic/test/WebHostTests.cs +++ b/src/Servers/Kestrel/Transport.Quic/test/WebHostTests.cs @@ -82,12 +82,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests o.Listen(IPAddress.Parse("127.0.0.1"), http3Port, listenOptions => { listenOptions.Protocols = Core.HttpProtocols.Http3; - listenOptions.UseHttps(); + listenOptions.UseHttps(TestResources.GetTestCertificate()); }); o.Listen(IPAddress.Parse("127.0.0.1"), http1Port, listenOptions => { listenOptions.Protocols = Core.HttpProtocols.Http1; - listenOptions.UseHttps(); + listenOptions.UseHttps(TestResources.GetTestCertificate()); }); }) .Configure(app => @@ -122,7 +122,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests o.Listen(IPAddress.Parse("127.0.0.1"), 5005, listenOptions => { listenOptions.Protocols = Core.HttpProtocols.Http1AndHttp2AndHttp3; - listenOptions.UseHttps(); + listenOptions.UseHttps(TestResources.GetTestCertificate()); }); }) .Configure(app => @@ -157,7 +157,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests o.Listen(IPAddress.Parse("127.0.0.1"), 0, listenOptions => { listenOptions.Protocols = Core.HttpProtocols.Http1AndHttp2AndHttp3; - listenOptions.UseHttps(); + listenOptions.UseHttps(TestResources.GetTestCertificate()); }); }) .Configure(app => @@ -225,7 +225,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests o.Listen(IPAddress.Parse("127.0.0.1"), 0, listenOptions => { listenOptions.Protocols = Core.HttpProtocols.Http1AndHttp2AndHttp3; - listenOptions.UseHttps(); + listenOptions.UseHttps(TestResources.GetTestCertificate()); }); }) .Configure(app => @@ -305,6 +305,42 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests } } + [ConditionalFact] + [MsQuicSupported] + public async Task StartAsync_Http3WithNonIPListener_ThrowError() + { + // Arrange + var builder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseKestrel(o => + { + o.ListenUnixSocket("/test-path", listenOptions => + { + listenOptions.Protocols = Core.HttpProtocols.Http3; + listenOptions.UseHttps(TestResources.GetTestCertificate()); + }); + }) + .Configure(app => + { + app.Run(async context => + { + await context.Response.WriteAsync("hello, world"); + }); + }); + }) + .ConfigureServices(AddTestLogging); + + using var host = builder.Build(); + + // Act + var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => host.StartAsync()).DefaultTimeout(); + + // Assert + Assert.Equal("QUIC doesn't support listening on the configured endpoint type. Expected IPEndPoint but got UnixDomainSocketEndPoint.", ex.Message); + } + private static HttpClient CreateClient() { var httpHandler = new HttpClientHandler(); diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs index 81d705f5f5942b2bad5ac7ddd011d125ff776d12..9ffe3c2a9f3d8f00e20e97afdc2d8546b34d1377 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs @@ -277,7 +277,7 @@ namespace Interop.FunctionalTests.Http3 using (var host = builder.Build()) using (var client = Http3Helpers.CreateClient()) { - await host.StartAsync(); + await host.StartAsync().DefaultTimeout(); var requestContent = new StreamingHttpContext(); @@ -289,22 +289,22 @@ namespace Interop.FunctionalTests.Http3 // 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(); } }