Skip to content

Commit 2e8d295

Browse files
authored
HTTP/3: Initial QUIC transport tests (#33989)
1 parent 299840a commit 2e8d295

File tree

12 files changed

+337
-12
lines changed

12 files changed

+337
-12
lines changed

src/Servers/Kestrel/Core/src/Properties/AssemblyInfo.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Server.Kestrel.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
77
[assembly: InternalsVisibleTo("Libuv.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
88
[assembly: InternalsVisibleTo("Sockets.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
9+
[assembly: InternalsVisibleTo("Quic.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
910
[assembly: InternalsVisibleTo("InMemory.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
1011
[assembly: InternalsVisibleTo("Sockets.BindTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
1112
[assembly: InternalsVisibleTo("Libuv.BindTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Runtime.CompilerServices;
55
using System.Runtime.Versioning;
66

7+
[assembly: InternalsVisibleTo("Quic.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
78
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
89
[assembly: SupportedOSPlatform("windows")]
910
[assembly: SupportedOSPlatform("macos")]

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ public QuicConnectionListener(QuicTransportOptions options, IQuicTrace log, EndP
3333

3434
_log = log;
3535
_context = new QuicTransportContext(_log, options);
36-
EndPoint = endpoint;
3736
var quicListenerOptions = new QuicListenerOptions();
3837

3938
// TODO Should HTTP/3 specific ALPN still be global? Revisit whether it can be statically set once HTTP/3 is finalized.
@@ -44,6 +43,9 @@ public QuicConnectionListener(QuicTransportOptions options, IQuicTrace log, EndP
4443
quicListenerOptions.IdleTimeout = options.IdleTimeout;
4544

4645
_listener = new QuicListener(QuicImplementationProviders.MsQuic, quicListenerOptions);
46+
47+
// Listener endpoint will resolve an ephemeral port, e.g. 127.0.0.1:0, into the actual port.
48+
EndPoint = _listener.ListenEndPoint;
4749
}
4850

4951
public EndPoint EndPoint { get; set; }
@@ -71,14 +73,13 @@ public ValueTask DisposeAsync()
7173
{
7274
if (_disposed)
7375
{
74-
return new ValueTask();
76+
return ValueTask.CompletedTask;
7577
}
7678

77-
_disposed = true;
78-
7979
_listener.Dispose();
80+
_disposed = true;
8081

81-
return new ValueTask();
82+
return ValueTask.CompletedTask;
8283
}
8384
}
8485
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,6 @@ private void CancelConnectionClosedToken()
216216
}
217217
}
218218

219-
220219
private async Task DoSend()
221220
{
222221
Exception? shutdownReason = null;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ internal class QuicTrace : IQuicTrace
2626
private static readonly Action<ILogger, string, string, Exception?> _streamAborted =
2727
LoggerMessage.Define<string, string>(LogLevel.Debug, new EventId(8, "StreamAbort"), @"Stream id ""{ConnectionId}"" aborted by application because: ""{Reason}"".", skipEnabledCheck: true);
2828

29-
private ILogger _logger;
29+
private readonly ILogger _logger;
3030

3131
public QuicTrace(ILogger logger)
3232
{

src/Servers/Kestrel/Transport.Quic/test/Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99
<ItemGroup>
1010
<Compile Include="$(SharedSourceRoot)NullScope.cs" />
1111
<Compile Include="$(KestrelSharedSourceRoot)test\*.cs" LinkBase="shared" />
12+
<Compile Include="$(KestrelSharedSourceRoot)test\TransportTestHelpers\MsQuicSupportedAttribute.cs" LinkBase="shared\TransportTestHelpers\MsQuicSupportedAttribute.cs" />
13+
<Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.pfx" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
1214
</ItemGroup>
1315

1416
<ItemGroup>
1517
<Reference Include="Microsoft.AspNetCore.Http" />
1618
<Reference Include="Microsoft.AspNetCore.Server.Kestrel.Core" />
1719
<Reference Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Quic" />
1820
<Reference Include="Microsoft.Extensions.Logging" />
19-
<Reference Include="System.Net.Experimental.MsQuic" />
2021
</ItemGroup>
2122

2223
</Project>
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Buffers;
6+
using System.Collections.Generic;
7+
using System.Net.Quic;
8+
using System.Text;
9+
using System.Threading.Tasks;
10+
using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests;
11+
using Microsoft.AspNetCore.Testing;
12+
using Xunit;
13+
14+
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests
15+
{
16+
public class QuicConnectionContextTests
17+
{
18+
private static readonly byte[] TestData = Encoding.UTF8.GetBytes("Hello world");
19+
20+
[ConditionalFact]
21+
[MsQuicSupported]
22+
public async Task AcceptAsync_ClientStartsAndStopsUnidirectionStream_ServerAccepts()
23+
{
24+
// Arrange
25+
await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory();
26+
27+
var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint);
28+
using var quicConnection = new QuicConnection(QuicImplementationProviders.MsQuic, options);
29+
await quicConnection.ConnectAsync().DefaultTimeout();
30+
31+
await using var serverConnection = await connectionListener.AcceptAsync().DefaultTimeout();
32+
33+
// Act
34+
var acceptTask = serverConnection.AcceptAsync();
35+
36+
await using var clientStream = quicConnection.OpenUnidirectionalStream();
37+
await clientStream.WriteAsync(TestData);
38+
39+
await using var serverStream = await acceptTask.DefaultTimeout();
40+
41+
// Assert
42+
Assert.NotNull(serverStream);
43+
Assert.False(serverStream.ConnectionClosed.IsCancellationRequested);
44+
45+
var closedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
46+
serverStream.ConnectionClosed.Register(() => closedTcs.SetResult());
47+
48+
// Read data from client.
49+
var read = await serverStream.Transport.Input.ReadAtLeastAsync(TestData.Length).DefaultTimeout();
50+
Assert.Equal(TestData, read.Buffer.ToArray());
51+
serverStream.Transport.Input.AdvanceTo(read.Buffer.End);
52+
53+
// Shutdown client.
54+
clientStream.Shutdown();
55+
56+
// Receive shutdown on server.
57+
read = await serverStream.Transport.Input.ReadAsync().DefaultTimeout();
58+
Assert.True(read.IsCompleted);
59+
60+
await closedTcs.Task.DefaultTimeout();
61+
}
62+
63+
[ConditionalFact]
64+
[MsQuicSupported]
65+
public async Task AcceptAsync_ClientStartsAndStopsBidirectionStream_ServerAccepts()
66+
{
67+
// Arrange
68+
await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory();
69+
70+
var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint);
71+
using var quicConnection = new QuicConnection(QuicImplementationProviders.MsQuic, options);
72+
await quicConnection.ConnectAsync().DefaultTimeout();
73+
74+
var serverConnection = await connectionListener.AcceptAsync().DefaultTimeout();
75+
76+
// Act
77+
var acceptTask = serverConnection.AcceptAsync();
78+
79+
await using var clientStream = quicConnection.OpenBidirectionalStream();
80+
await clientStream.WriteAsync(TestData);
81+
82+
await using var serverStream = await acceptTask.DefaultTimeout();
83+
await serverStream.Transport.Output.WriteAsync(TestData);
84+
85+
// Assert
86+
Assert.NotNull(serverStream);
87+
Assert.False(serverStream.ConnectionClosed.IsCancellationRequested);
88+
89+
var closedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
90+
serverStream.ConnectionClosed.Register(() => closedTcs.SetResult());
91+
92+
// Read data from client.
93+
var read = await serverStream.Transport.Input.ReadAtLeastAsync(TestData.Length).DefaultTimeout();
94+
Assert.Equal(TestData, read.Buffer.ToArray());
95+
serverStream.Transport.Input.AdvanceTo(read.Buffer.End);
96+
97+
// Read data from server.
98+
var data = new List<byte>();
99+
var buffer = new byte[1024];
100+
var readCount = 0;
101+
while ((readCount = await clientStream.ReadAsync(buffer).DefaultTimeout()) != -1)
102+
{
103+
data.AddRange(buffer.AsMemory(0, readCount).ToArray());
104+
if (data.Count == TestData.Length)
105+
{
106+
break;
107+
}
108+
}
109+
Assert.Equal(TestData, data);
110+
111+
// Shutdown from client.
112+
clientStream.Shutdown();
113+
114+
// Get shutdown from client.
115+
read = await serverStream.Transport.Input.ReadAsync().DefaultTimeout();
116+
Assert.True(read.IsCompleted);
117+
118+
await closedTcs.Task.DefaultTimeout();
119+
}
120+
121+
[ConditionalFact]
122+
[MsQuicSupported]
123+
public async Task AcceptAsync_ServerStartsAndStopsUnidirectionStream_ClientAccepts()
124+
{
125+
// Arrange
126+
await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory();
127+
128+
var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint);
129+
using var quicConnection = new QuicConnection(QuicImplementationProviders.MsQuic, options);
130+
await quicConnection.ConnectAsync().DefaultTimeout();
131+
132+
var serverConnection = await connectionListener.AcceptAsync().DefaultTimeout();
133+
134+
// Act
135+
var acceptTask = quicConnection.AcceptStreamAsync();
136+
137+
await using var serverStream = await serverConnection.ConnectAsync();
138+
await serverStream.Transport.Output.WriteAsync(TestData).DefaultTimeout();
139+
140+
await using var clientStream = await acceptTask.DefaultTimeout();
141+
142+
// Assert
143+
Assert.NotNull(clientStream);
144+
145+
// Read data from server.
146+
var data = new List<byte>();
147+
var buffer = new byte[1024];
148+
var readCount = 0;
149+
while ((readCount = await clientStream.ReadAsync(buffer).DefaultTimeout()) != -1)
150+
{
151+
data.AddRange(buffer.AsMemory(0, readCount).ToArray());
152+
if (data.Count == TestData.Length)
153+
{
154+
break;
155+
}
156+
}
157+
Assert.Equal(TestData, data);
158+
159+
// Complete server.
160+
await serverStream.Transport.Output.CompleteAsync().DefaultTimeout();
161+
162+
// Receive complete in client.
163+
readCount = await clientStream.ReadAsync(buffer).DefaultTimeout();
164+
Assert.Equal(0, readCount);
165+
}
166+
}
167+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Net.Quic;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests;
8+
using Microsoft.AspNetCore.Testing;
9+
using Xunit;
10+
11+
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests
12+
{
13+
public class QuicConnectionListenerTests
14+
{
15+
[ConditionalFact]
16+
[MsQuicSupported]
17+
public async Task AcceptAsync_AfterUnbind_Error()
18+
{
19+
// Arrange
20+
await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory();
21+
22+
// Act
23+
await connectionListener.UnbindAsync().DefaultTimeout();
24+
25+
// Assert
26+
await Assert.ThrowsAsync<ObjectDisposedException>(() => connectionListener.AcceptAsync().AsTask()).DefaultTimeout();
27+
}
28+
29+
[ConditionalFact]
30+
[MsQuicSupported]
31+
public async Task AcceptAsync_ClientCreatesConnection_ServerAccepts()
32+
{
33+
// Arrange
34+
await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory();
35+
36+
// Act
37+
var acceptTask = connectionListener.AcceptAsync().DefaultTimeout();
38+
39+
var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint);
40+
41+
using var quicConnection = new QuicConnection(QuicImplementationProviders.MsQuic, options);
42+
await quicConnection.ConnectAsync().DefaultTimeout();
43+
44+
// Assert
45+
await using var connection = await acceptTask.DefaultTimeout();
46+
Assert.False(connection.ConnectionClosed.IsCancellationRequested);
47+
48+
await connection.DisposeAsync().AsTask().DefaultTimeout();
49+
50+
// ConnectionClosed isn't triggered because the server initiated close.
51+
Assert.False(connection.ConnectionClosed.IsCancellationRequested);
52+
}
53+
}
54+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Net;
7+
using System.Net.Quic;
8+
using System.Net.Security;
9+
using System.Security.Cryptography.X509Certificates;
10+
using System.Threading;
11+
using System.Threading.Tasks;
12+
using Microsoft.AspNetCore.Http.Features;
13+
using Microsoft.AspNetCore.Server.Kestrel.Https;
14+
using Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal;
15+
using Microsoft.AspNetCore.Testing;
16+
using Microsoft.Extensions.Logging.Abstractions;
17+
using Microsoft.Extensions.Options;
18+
19+
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests
20+
{
21+
internal static class QuicTestHelpers
22+
{
23+
public static QuicTransportFactory CreateTransportFactory()
24+
{
25+
var quicTransportOptions = new QuicTransportOptions();
26+
quicTransportOptions.Alpn = "h3-29";
27+
quicTransportOptions.IdleTimeout = TimeSpan.FromMinutes(1);
28+
29+
return new QuicTransportFactory(NullLoggerFactory.Instance, Options.Create(quicTransportOptions));
30+
}
31+
32+
public static async Task<QuicConnectionListener> CreateConnectionListenerFactory()
33+
{
34+
var transportFactory = CreateTransportFactory();
35+
36+
// Use ephemeral port 0. OS will assign unused port.
37+
var endpoint = new IPEndPoint(IPAddress.Loopback, 0);
38+
39+
var features = CreateBindAsyncFeatures();
40+
return (QuicConnectionListener)await transportFactory.BindAsync(endpoint, features, cancellationToken: CancellationToken.None);
41+
}
42+
43+
public static FeatureCollection CreateBindAsyncFeatures()
44+
{
45+
var cert = TestResources.GetTestCertificate();
46+
47+
var sslServerAuthenticationOptions = new SslServerAuthenticationOptions();
48+
sslServerAuthenticationOptions.ServerCertificate = cert;
49+
sslServerAuthenticationOptions.RemoteCertificateValidationCallback = RemoteCertificateValidationCallback;
50+
51+
var features = new FeatureCollection();
52+
features.Set(sslServerAuthenticationOptions);
53+
54+
return features;
55+
}
56+
57+
private static bool RemoteCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
58+
{
59+
return true;
60+
}
61+
62+
public static QuicClientConnectionOptions CreateClientConnectionOptions(EndPoint remoteEndPoint)
63+
{
64+
return new QuicClientConnectionOptions
65+
{
66+
MaxBidirectionalStreams = 10,
67+
MaxUnidirectionalStreams = 20,
68+
RemoteEndPoint = remoteEndPoint,
69+
ClientAuthenticationOptions = new SslClientAuthenticationOptions
70+
{
71+
ApplicationProtocols = new List<SslApplicationProtocol>
72+
{
73+
new SslApplicationProtocol("h3-29")
74+
},
75+
RemoteCertificateValidationCallback = RemoteCertificateValidationCallback
76+
}
77+
};
78+
}
79+
}
80+
}

0 commit comments

Comments
 (0)