-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
Background and motivation
API design for exposing QuicListener and related classes to the public.
There are 2 major design approaches. The first one requires options for incoming connection (to finish the handshake) during listener creation. The alternative doesn't expect these options until Accept is called. Both solutions take into account #49587.
Options Upfront
This design was discussed in our team and is based on what we have now. Thus it is safer since we have at least some experience with this shape.
- It does not allow for meaningful state object for
ServerOptionsSelectionCallbacksince it'd need to be provided upfront (one object per listener). - It'll finish handshake for all incoming connections regardless whether there's anyone ready to accept them.
Related issues:
- HTTP/3: QuicConnectionListener supports ServerOptionsSelectionCallback #49587
Dynamic selection of SSL options viaServerOptionsSelectionCallback - QUIC: Validate SslServerAuthenticationOptions set on QuicListenerOptions #49423
ALPN needs to be provided upfront
API Proposal
// Change from the current class is removal of all the constructors which are replaced with static Create method.
// All the Quic classes will have static Create so that they can became abstract in the future.
// Also all the overloads accepting implementation provider (temporary) are removed.
public class QuicListener : IAsyncDisposable
{
public IPEndPoint ListenEndPoint { get; }
public async ValueTask<QuicConnection> AcceptConnectionAsync(CancellationToken cancellationToken = default);
public void DisposeAsync();
}
// QuicServerConnectionOptions in https://github.com/dotnet/runtime/issues/68902
public delegate ValueTask<QuicServerConnectionOptions> ServerOptionsSelectionCallback(QuicConnection connection, SslClientHelloInfo clientHelloInfo, CancellationToken cancellationToken);
// Listener options necessary for starting a listener.
public class QuicListenerOptions
{
public IPEndPoint ListenEndPoint { get; set; }
public List<SslApplicationProtocol> ApplicationProtocols { get; set; }
public int ListenBacklog { get; set; } = 512;
public ServerOptionsSelectionCallback ConnectionOptionsCallback { get; set; }
}API Usage
var connectionOptions = new QuicServerConnectionOptions()
{
IdleTimeout = TimeSpan.FromMinutes(5),
MaxBidirectionalStreams = 1000,
MaxUnidirectionalStreams = 10,
ServerSslOptions = new SslServerAuthenticationOptions()
{
ApplicationProtocols = new List<SslApplicationProtocol>(){ SslApplicationProtocol.Http3 },
ServerCertificate = TestCertificateExtensions.ServerCertificate,
}
};
await using var listener = await QuicProvider.CreateListenerAsync(new QuicListenerOptions()
{
ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 5000),
ApplicationProtocols = { SslApplicationProtocol.Http3 },
ListenBacklog = 1024,
ConnectionOptionsCallback = (connection, clientHelloInfo, cancellationToken) =>
{
// ...
return new ValueTask<ConnectionOptions>(connectionOptions);
},
}, cancellationToken);
while (running)
{
await using var connection = await listener.AcceptConnectionAsync(cancellationToken);
// More work with connection
}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.
cc: @JamesNK @Tratcher @wfurt @CarnaViire @rzikm
EDIT: Hid Options in Accept since Options Upfront are the preferred design. Also removed stateObject for the callback.
Alternative Designs
Static QuicServerConnectionOptions
Apart from ServerOptionsSelectionCallback, QuicListenerOptions could also have direct property for QuicServerConnectionOptions, bypassing the callback. Since it can always be provided via the callback, there's no functional need for it at the moment. Also ASP.NET Core needs the callback so we don't even have a use for it now.
QuicServerConnectionOptions in Accept
This design came out from my experiments with listener shape. It's been prototyped but it's unproven design.
- It allows meaningful state object for
ServerOptionsSelectionCallbackcallback. - It makes accept method cluttered with parameters.
- It allows for different options per connection
- Unless there's an accept pending, the connection handshake might timeout (default is 1/3 of idle timeout) while the connection waits for someone to accept it. As a result, we might discard connections which we would have otherwise accepted.
- On the other hand, we wouldn't waste cycles on finishing handshake for connections which we don't accept in the end.
API Proposal
/// <summary>Represents server side of QUIC transport. Listens for incoming QuicConnections.</summary>
public sealed class QuicListener : IAsyncDisposable
{
// Static create method instead of ctor so that the class can became abstract in the future.
public static QuicListener Create(QuicListenerOptions options);
public IPEndPoint ListenEndPoint;
// Accept methods now have SSL and connection options as extra arguments.
// Those options are connection specific and there's no need to provide them at the time of listener creation.
// This also allows us to pass sslOptionsCallbackState
public ValueTask<QuicConnection> AcceptConnectionAsync(ServerSslOptionsSelectionCallback sslOptionsCallback, object? sslOptionsCallbackState, QuicConnectionOptions connectionOptions, CancellationToken cancellationToken = default);
public ValueTask<QuicConnection> AcceptConnectionAsync(SslServerAuthenticationOptions sslOptions, QuicConnectionOptions connectionOptions, CancellationToken cancellationToken = default);
public ValueTask DisposeAsync();
}
public delegate ValueTask<SslServerAuthenticationOptions> ServerSslOptionsSelectionCallback(QuicConnection connection, object? state, SslClientHelloInfo clientHelloInfo, CancellationToken cancellationToken);
public class QuicListenerOptions
{
/// <summary>The local endpoint to listen on.</summary>
[Required]
public IPEndPoint ListenEndPoint { get; init; } = null!;
/// <summary>The application protocols.</summary>
[Required]
public SslApplicationProtocol[] ApplicationProtocols { get; init; } = Array.Empty<SslApplicationProtocol>();
/// <summary>Number of connections to be held without accepting the connection.</summary>
public int ListenBacklog { get; init; } = 512;
}
/// <summary>Options for a new connection, the same options are used for incoming and outgoing connections.</summary>
public class QuicConnectionOptions
{
/// <summary>Limit on the number of bidirectional streams the remote peer connection can create on an open connection.</summary>
public int MaxBidirectionalStreams { get; init; } = 100;
/// <summary>Limit on the number of unidirectional streams the remote peer connection can create on an open connection.</summary>
public int MaxUnidirectionalStreams { get; init; } = 100;
/// <summary>Idle timeout for connections, after which the connection will be closed.</summary>
public TimeSpan IdleTimeout { get; init; } = TimeSpan.FromMinutes(2);
// This class will potentially expand with other connection options which we'll deem interesting for user to set.
}API Usage
await using var listener = QuicListener.Create(new QuicListenerOptions() {
ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 5000),
ApplicationProtocols = { SslApplicationProtocol.Http3 },
ListenBacklog = 1024,
});
var connectionOptions = new QuicConnectionOptions() {
IdleTimeout = TimeSpan.FromMinutes(5),
MaxBidirectionalStreams = 1000,
MaxUnidirectionalStreams = 10
},
var serverSslOptions = new SslServerAuthenticationOptions() {
ApplicationProtocols = new List<SslApplicationProtocol>(){ SslApplicationProtocol.Http3 },
ServerCertificate = TestCertificateExtensions.ServerCertificate
};
var serverSslOptionsSelectionCallback = (connection, clientHelloInfo, state, cancellationToken) => {
// ...
return new ValueTask<SslServerAuthenticationOptions>(serverSslOptions);
};
while (running) {
// Either this way
await using var connection = await listener.AcceptConnectionAsync(serverSslOptions, connectionOptions, cancellationToken);
// Or that way
await using var connection = await listener.AcceptConnectionAsync(serverSslOptionsSelectionCallback, null, connectionOptions, cancellationToken);
// More work with connection
}