-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
Background and motivation
QuicListenerOptions.ConnectionOptionsCallback
is a callback that allows the server to modify connection options when it accepts a connection. One of its arguments is QuicConnection
.
runtime/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListenerOptions.cs
Lines 33 to 36 in a54a823
/// <summary> | |
/// Selection callback to choose inbound connection options dynamically. | |
/// </summary> | |
public Func<QuicConnection, SslClientHelloInfo, CancellationToken, ValueTask<QuicServerConnectionOptions>> ConnectionOptionsCallback { get; set; } = null!; |
In ASP.NET Core we allow people to hook into this callback, although we wrap it in our own signature because we have an abstraction called ConnectionContext
for a server connection that's not specific to QUIC or HTTP/3.
_quicListenerOptions = new QuicListenerOptions
{
ApplicationProtocols = _tlsConnectionOptions.ApplicationProtocols,
ListenEndPoint = listenEndPoint,
ListenBacklog = options.Backlog,
ConnectionOptionsCallback = async (quicConnection, helloInfo, cancellationToken) =>
{
var serverAuthenticationOptions = await _tlsConnectionOptions.OnConnection(new TlsConnectionCallbackContext
{
CancellationToken = cancellationToken,
ClientHelloInfo = helloInfo,
State = _tlsConnectionOptions.OnConnectionState,
Connection = new QuicConnectionContext(quicConnection, _context), // wrap QuicConnection with our abstraction here
});
var connectionOptions = new QuicServerConnectionOptions
{
ServerAuthenticationOptions = serverAuthenticationOptions,
IdleTimeout = options.IdleTimeout,
MaxInboundBidirectionalStreams = options.MaxBidirectionalStreamCount,
MaxInboundUnidirectionalStreams = options.MaxUnidirectionalStreamCount,
DefaultCloseErrorCode = 0,
DefaultStreamErrorCode = 0,
};
return connectionOptions;
}
}
Once the connection is accepted and returned from
var quicConnection = await _listener.AcceptConnectionAsync(cancellationToken);
var connectionContext = new QuicConnectionContext(quicConnection, _context);
The code above is creating our abstraction - QuicConnectionContext
- twice: once in the callback and then again after AcceptConnectionAsync
returns. We don't want to do this because of allocations, but also because someone can update state on our connection abstraction, and that would disappear if it was recreated.
It would be better if we were able to create QuicConnectionContext
once inside the callback, use it, then add it to a property bag on QuicConnection
, and then use that value when AcceptConnectionAsync
returns.
var quicConnection = await _listener.AcceptConnectionAsync(cancellationToken);
var connectionContext = (QuicConnectionContext)quicConnection.Properties[QuicConnectionContextKey];
Other options include adding QuicConnectionContext
to a ConditionalWeakTable<QuicConnection, QuicConnectionContext>
inside the callback, then removing it outside the callback. But a place to stash state on the QuicConnection
would be much simpler.
This isn't critical for .NET 7. Workarounds include using a weak table, allocating twice (which fixes one problem but creates another...), or leaving ConnectionContext
null in the callback in .NET 7 and waiting for this API in .NET 8.
API Proposal
namespace System.Net.Quic;
public class QuicConnection
{
public IDictionary<string, object?> Properties { get; }
}
API Usage
_quicListenerOptions = new QuicListenerOptions
{
ApplicationProtocols = _tlsConnectionOptions.ApplicationProtocols,
ListenEndPoint = listenEndPoint,
ListenBacklog = options.Backlog,
ConnectionOptionsCallback = async (quicConnection, helloInfo, cancellationToken) =>
{
quicConnection.Properties[QuicConnectionContextKey] = new QuicConnectionContext(quicConnection, _context);
// ...figure out conneciton options...
return connectionOptions;
}
}
var quicConnection = await _listener.AcceptConnectionAsync(cancellationToken);
var connectionContext = (QuicConnectionContext)quicConnection.Properties[QuicConnectionContextKey];
Alternative Designs
Add a state argument to ConnectionOptionsCallback
callback and AcceptConnectionAsync
.
_quicListenerOptions = new QuicListenerOptions
{
ApplicationProtocols = _tlsConnectionOptions.ApplicationProtocols,
ListenEndPoint = listenEndPoint,
ListenBacklog = options.Backlog,
ConnectionOptionsCallback = async (quicConnection, helloInfo, state, cancellationToken) =>
{
var listenerContext = (QuicListenerContext)state!
listenerContext._acceptingQuicConnectionContext = new QuicConnectionContext(quicConnection, _context);
// ...figure out conneciton options...
return connectionOptions;
}
}
var quicConnection = await _listener.AcceptConnectionAsync(cancellationToken, state: this);
var connectionContext = _acceptingQuicConnectionContext; // was set in callback
Risks
No response