Skip to content

[API Proposal]: [QUIC] QuicConnection #68902

@ManickaP

Description

@ManickaP

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:

Discussed Considerations

  1. StreamCount/OpenStream parametrize by stream type instead of sets of 2 methods
    --> YES
  2. Create with endpoint parameters or put them into options?
    a) inside options in ConnectAsync fits better with Socket/SslStream
    --> YES, also keep them non-nullable and throw if not connected yet
    b) inside ctor allows us to have RemoteEndPoint non-nullable and better aligns with incoming connections (they have both side address, but are not "configured" with options yet)
    --> NO
  3. 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 Socket does, 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 in ConnectAsync
      --> Properties are IPEndPoint, provided is EndPoint that can also be DnsEndPoint
  4. Options in ConnectAsync - we don't need them past the connection establishement moment so putting them into Create doesn't make much sense
    • we might consider collapsing Create + ConnectAsync (==> we might consider making QuicListener Create async as well)
    • we might keep Create parameter less, put everything to options and let ConnectAsync deal with it, this is not consistent with QuicListener though (does it matter or not?)
      --> YES, options in ConnectAsync, Create is ctor replacement
  5. 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

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.

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarea-System.Net.QuicblockingMarks issues that we want to fast track in order to unblock other important work

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions