-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
Background and motivation
API design for exposing QuicConnection and related classes to the public.
The API shape is based on the current internal shape of the class with the exception of merging the ConnectAsync into QuicProvider.CreateConnectionAsync.
Related issues:
-
QUIC API: Consider passing QuicConnectionOptions to Quic.Connection.ConnectAsync #56009
Consider passing QuicConnectionOptions to Quic.Connection.ConnectAsync: we don't need to hold them just for the meantime between ctor andConnectAsync -
[QUIC] Rewrite Open(Uni|Bidi)rectionalStream to leverage async stream opening #67302
Async stream open: perf need, coming from our benchmarks -
HTTP/3: Create additional connections when maximum active streams is reached #51775
Multiple connections: same as H/2 requirement -
[API Proposal]: QuicConnection.CancelPendingAcceptStream #60818
CancelPendingAcceptStream: used only for closing the connection, it's not as hot path as originally thought, I'd prefer not to do it now. -
[API Proposal]: Expose TLS details for QUIC connection #70184
TLS details for QUIC connection
Discussed Considerations
- StreamCount/OpenStream parametrize by stream type instead of sets of 2 methods
--> YES - Create with endpoint parameters or put them into options?
a) inside options inConnectAsyncfits better withSocket/SslStream
--> YES, also keep them non-nullable and throw if not connected yet
b) inside ctor allows us to haveRemoteEndPointnon-nullable and better aligns with incoming connections (they have both side address, but are not "configured" with options yet)
--> NO - Endpoints are IP endpoints, need to consider on which level we'll do DNS since MsQuic has only crude resolution
- to able to do what
Socketdoes, means that we need to create MsQuic connection object, try to connect it, let it fail, release it and then repeat for another IP - the resolution cannot be done in
Create, it would need to be done inConnectAsync
--> Properties areIPEndPoint, provided isEndPointthat can also beDnsEndPoint
- to able to do what
- Options in
ConnectAsync- we don't need them past the connection establishement moment so putting them intoCreatedoesn't make much sense- we might consider collapsing
Create+ConnectAsync(==> we might consider makingQuicListenerCreateasync as well) - we might keep
Createparameter less, put everything to options and letConnectAsyncdeal with it, this is not consistent withQuicListenerthough (does it matter or not?)
--> YES, options inConnectAsync,Createis ctor replacement
- we might consider collapsing
- Multiple connections scenario: we'll need something like
TryOpenStreamAsync+WaitForStreamAsync(not prototyped yet)
API Proposal
namespace System.Net.Quic;
public sealed class QuicConnection : IAsyncDisposable
{
/// <summary>Returns true if QUIC is supported and can be used, e.g. msquic is present, high enough version of TLS is available etc.</summary>
public static bool IsSupported { get; }
/// <summary>Creates new, fully connected connection configured with the provided options.</summary>
/// <exception cref="PlatformNotSupportedException">When <see cref="IsSupported" /> is <c>false</c>.</exception>
public static ValueTask<QuicConnection> ConnectAsync(QuicConnectionOptions options, CancellationToken cancellationToken = default);
/// <summary>Remote endpoint to which the connection is connected.</summary>
public IPEndPoint RemoteEndPoint { get; }
/// <summary>Local endpoint to which the connection is bound.</summary>
public IPEndPoint LocalEndPoint { get; }
/// <summary>Peer's certificate, available only if the peer provided the certificate.</summary>
public X509Certificate2? RemoteCertificate { get; }
/// <summary>Final, negotiated ALPN.</summary>
public SslApplicationProtocol NegotiatedApplicationProtocol { get; }
/// <summary>
/// Create an outbound uni/bidirectional stream.
/// </summary>
public ValueTask<QuicStream> OpenOutboundStreamAsync(QuicStreamType type, CancellationToken cancellationToken = default);
/// <summary>
/// Accept an inbound stream.
/// </summary>
public ValueTask<QuicStream> AcceptInboundStreamAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Close the connection and terminate any active streams.
/// </summary>
public ValueTask CloseAsync(long errorCode, CancellationToken cancellationToken = default);
/// <summary>
/// Silently closes the connection if not closed with CloseAsync beforehand.
/// </summary>
public void DisposeAsync();
}
/// <summary>Options for a new connection, the same options are used for incoming and outgoing connections.</summary>
public abstract class QuicConnectionOptions
{
/// <summary>Prevent user sub-classing.</summary>
internal QuicConnectionOptions()
{}
/// <summary>Limit on the number of bidirectional streams the remote peer connection can create on an open connection.</summary>
public int MaxInboundBidirectionalStreams { get; set; }
/// <summary>Limit on the number of unidirectional streams the remote peer connection can create on an open connection.</summary>
public int MaxInboundUnidirectionalStreams { get; set; }
/// <summary>Idle timeout for connections, after which the connection will be closed. Zero means using default of the underlying implementation.</summary>
public TimeSpan IdleTimeout { get; set; } = TimeSpan.Zero;
/// <summary>Error code used when the stream needs to abort read or write side of the stream internally.</summary>
public required long DefaultStreamErrorCode { get; set; }
}
/// <summary>Options for a new connection, only used for outbound connections.</summary>
public sealed class QuicClientConnectionOptions : QuicConnectionOptions
{
/// <summary>SSL options for the outgoing connection.</summary>
public required SslClientAuthenticationOptions ClientAuthenticationOptions { get; set; }
/// <summary>The endpoint to connect to.</summary>
public required EndPoint RemoteEndPoint { get; set; }
/// <summary>Optional local endpoint from which the connection is to be established.</summary>
public IPEndPoint? LocalEndPoint { get; set; }
public QuicClientConnectionOptions()
{
MaxInboundBidirectionalStreams = 0;
MaxInboundUnidirectionalStreams = 0;
}
}
/// <summary>Options for a new connection, only used for incoming connections.</summary>
public sealed class QuicServerConnectionOptions : QuicConnectionOptions
{
/// <summary>SSL options for the incoming connection</summary>
public required SslServerAuthenticationOptions ServerAuthenticationOptions { get; set; }
public QuicServerConnectionOptions()
{
MaxInboundBidirectionalStreams = 100;
MaxInboundUnidirectionalStreams = 10;
}
}API Usage
Client usage:
var options = new QuicClientConnectionOptions()
{
RemoteEndPoint = new DnsEndPoint("localhost", 5001),
DefaultStreamErrorCode = (long)Http3ErrorCode.RequestCancelled,
ClientAuthenticationOptions = new SslClientAuthenticationOptions()
{
ApplicationProtocols = new List<SslApplicationProtocol>() { SslApplicationProtocol.Http3 },
}
};
await using var connection = await QuicProvider.CreateConnectionAsync(options, cancellationToken);
await using var stream = await connection.OpenStreamAsync(StreamDirection.Bidirectional, cancellationToken);
// Work with stream, open more of them, send and receive data, close them ... https://github.com/dotnet/runtime/issues/69675
// Close will terminate all unclosed streams.
// If not called, the peer side of the connection will have to wait for idle connection timeout.
await connection.CloseAsync((long)Http3ErrorCode.NoError, cancellationToken);
// DisposeAsync called by await using.Server usage:
// Consider listener from https://github.com/dotnet/runtime/issues/67560:
await using var connection = await listener.AcceptConnectionAsync(cancellationToken);
while (running)
{
// In case the client closes the connection, Accept will throw appropriate exception.
await using var stream = await connection.AcceptStreamAsync(cancellationToken);
// Send and receive data... https://github.com/dotnet/runtime/issues/69675
// DisposeAsync called by await using.
}
// Close will terminate all unclosed streams. Note that H/3 uses GO_AWAY to negotiate graceful connection shutdown with the client.
await connection.CloseAsync((long)Http3ErrorCode.NoError, cancellationToken);
// DisposeAsync called by await using.Alternative Designs
- See Discussed Considerations
- As per [API Proposal]: Expose TLS details for QUIC connection #70184, TLS related properties could be combined into a common struct/class or left as individual properties on
QuicConnection.
Risks
As I'll state with all QUIC APIs. We might consider making all of these PreviewFeature. Not to deter user from using it, but to give us flexibility to tune the API shape based on customer feedback.
We don't have many users now and we're mostly making these APIs based on what Kestrel needs, our limited experience with System.Net.Quic and my meager experiments with other QUIC implementations.