QUIC configuration options

The System.Net.Quic library uses options classes to configure the protocol objects (QuicListener and QuicConnection) before their construction and initialization. There are three different options classes to do that:

All of the options classes can be set up incrementally, meaning that they don't require any of their properties to be initialized via constructor and can be set up independently. But the moment they're used to configure a new listener or a connection, the options are validated and an appropriate type of ArgumentException is thrown for any missing mandatory values or misconfigured ones. For example, if mandatory QuicConnectionOptions.DefaultStreamErrorCode isn't set, calling ConnectAsync(QuicClientConnectionOptions, CancellationToken) throws ArgumentOutOfRangeException.

QuicListenerOptions

QuicListenerOptions are used in QuicListener.ListenAsync(QuicListenerOptions, CancellationToken) when starting a new QuicListener. The individual configuration properties are:

ApplicationProtocols

ApplicationProtocols define the application protocols accepted by the server (RFC 7301 - ALPN). It can contain multiple values for different protocols that can be unrelated. In the process of accepting a new connection, listener can narrow down or select one specific protocol for each incoming connection, see QuicListenerOptions.ConnectionOptionsCallback. This property is mandatory and must contains at least one value.

ConnectionOptionsCallback

ConnectionOptionsCallback is a delegate to choose QuicServerConnectionOptions for an incoming connection. The function is given a partially initialized instance of QuicConnection and SslClientHelloInfo containing the server name requested by the client (RFC 6066 - SNI). The delegate is invoked for each incoming connection. It can return different options based on the provided client info or it can safely return the same instance of the options every time. The delegate purpose and shape is intentionally similar to ServerOptionsSelectionCallback used in SslStream.AuthenticateAsServerAsync(ServerOptionsSelectionCallback, Object, CancellationToken). This property is mandatory.

ListenBacklog

ListenBacklog determines how many incoming connections can be held by the listener before additional ones start being refused. Every attempt to establish a connection counts, even when it fails or when the connection gets shut down while waiting in the queue. Ongoing processes to establish a new connection count towards this limit as well. Connections or connection attempts are counted until they're retrieved via QuicListener.AcceptConnectionAsync(CancellationToken). The purpose of the backlog limit is to prevent servers from being overwhelmed by more incoming connections than they can process. This property is optional, default value is 512.

ListenEndPoint

ListenEndPoint contains the IP address and port on which the listener will accept new connections. Due to underlying implementation, MsQuic, the listener, always binds to a dual-stack wildcard socket regardless of what's specified here. This can lead to some unexpected behaviors, especially in comparison with ordinary TCP sockets like in HTTP/1.1 and HTTP/2 cases. For more information, see QUIC Troubleshooting Guide. This property is mandatory.

QuicConnectionOptions

QuicConnectionOptions options are shared between QuicClientConnectionOptions and QuicServerConnectionOptions. It's an abstract base class and can't be used on its own. It contains these properties:

DefaultCloseErrorCode

DefaultCloseErrorCode is used when the connection is disposed without calling QuicConnection.CloseAsync(Int64, CancellationToken). It's required by QUIC protocol to provide an application-level reason for closing a connection (RFC 9000 - Connection Close). QuicConnection has no way to force application code to call CloseAsync(Int64, CancellationToken) before disposing the connection. In such case, the connection needs to know what error code to use. This property is mandatory.

DefaultStreamErrorCode

DefaultStreamErrorCode is used when a stream is disposed before all the data is read. When receiving data over QUIC stream, an application can either consume all the data or, if not, it needs to abort its reading side. And similarly to connection closing, QUIC protocol requires an application-level reason for aborting the reading side (RFC 9000 - Stop Sending). This property is mandatory.

HandshakeTimeout

HandshakeTimeout sets the time limit in which the connection must be fully established; otherwise, it gets aborted. It's possible to set this value to InfiniteTimeSpan but it's discouraged. Connection attempts might hang indefinitely and there are no means to clear them apart from stopping the QuicListener. This property is optional, default value is 10 seconds.

IdleTimeout

If the connection is inactive for more than the specified IdleTimeout, it gets disconnected. This option is part of the QUIC protocol specification (RFC 9000 - Idle Timeout) and is sent to the peer during connection handshake. The connection then takes the smaller of it and the peer's idle timeouts and uses that. Thus the connection can get closed on idle timeout sooner than what this option was set to. This property is optional, default value is based on MsQuic, which is 30 seconds.

InitialReceiveWindowSizes

InitialReceiveWindowSizes specifies a set of values limiting how much data, initially, can be received by the connection and/or the stream. QUIC protocol defines a mechanism to limit how much data can be sent over the individual streams as well as cumulatively for the whole connection (RFC 9000 - Data Flow Control). These limits only apply before the application starts consuming the data. After that, MsQuic continually adjusts the receive window's size based on how fast the application reads them. This property is of QuicReceiveWindowSizes type, which contains these options:

These values must be a non-negative integer that's a power of 2; this is an inherited limitation from MsQuic. Setting any of these values to 0 essentially means that no data will ever be received by the specific stream or a connection as a whole. This property is optional, default values are 64 KB for a stream and 64 MB for a connection.

KeepAliveInterval

KeepAliveInterval determines if and how often PING frames are sent to keep the connection active and prevent it being closed on IdleTimeout (RFC 9000 - PING Frames). If setting this property, consider recommendation from RFC 9000 - Deferring Idle Timeout. Setting the value too low might negatively impact the performance. Also, setting the property too close to idle timeout might still lead to connection closures. This property is optional, default value is InfiniteTimeSpan meaning no PINGs will be sent.

MaxInboundBidirectionalStreams

MaxInboundBidirectionalStreams determines the maximum number of concurrently active bidirectional streams that the connection is willing to accept. Note that this differs from how QUIC specification defines handling concurrency (RFC 9000 - Controlling Concurrency). The QUIC protocol counts the streams cumulatively, over the connection lifetime, and uses an ever-increasing limit to determine the overall number of streams accepted by the connection, including already closed streams (RFC 9000 - MAX_STREAMS Frames). This property simplifies this so that the application only specifies the concurrent stream limit and MsQuic takes care of translating this limit to the corresponding MAX_STREAMS frames. This property is optional, default value is 0 for client connections and 100 for server connections.

MaxInboundUnidirectionalStreams

MaxInboundUnidirectionalStreams determines the maximum number of concurrently active unidirectional streams that the connection is willing to accept. Note that this differs from how QUIC specification defines handling stream concurrency (RFC 9000 - Controlling Concurrency). The QUIC protocol counts the streams cumulatively, over the connection lifetime, and uses an ever-increasing limit to determine the overall number of streams accepted by the connection, including already closed streams (RFC 9000 - MAX_STREAMS Frames). This property simplifies this so that the application only specifies the concurrent stream limit and MsQuic takes care of translating this limit to the corresponding MAX_STREAMS frames. This property is optional, default value is 0 for client connections and 10 for server connections.

StreamCapacityCallback

StreamCapacityCallback is a callback that's invoked whenever the peer releases a new stream capacity via MAX_STREAMS and as a result, the current capacity is above 0. The values provided in the callback arguments are capacity increments, meaning that the sum of all values from the callback will equal the last value received from MAX_STREAMS (RFC 9000 - MAX_STREAMS Frames). This callback was designed to support SocketsHttpHandler.EnableMultipleHttp3Connections functionality and comes with several caveats:

The following simplified scenario captures the behavior of stream opening and the callback:

  1. Client initiates connection to the server via:

    var client = await QuicConnection.ConnectAsync(new QuicClientConnectionOptions
    {
        ...
        StreamCapacityCallback = (connection, args) =>
            Console.WriteLine($"{connection} stream capacity increased by: unidi += {args.UnidirectionalIncrement}, bidi += {args.BidirectionalIncrement}")
    };
    
  2. Server sends initial settings to client with the stream limit 2 for unidirectional streams and 0 for bidirectional.

  3. The client's StreamCapacityCallback is called and prints:

    [conn][0x58575BF805B0] stream capacity increased by: unidi += 2, bidi += 0
    
  4. The client call to ConnectAsync returns with [conn][0x58575BF805B0] connection.

  5. The client attempts to open a few streams:

    var stream1 = await connection.OpenOutboundStreamAsync(QuicStreamType.Unidirectional);
    var stream2 = await connection.OpenOutboundStreamAsync(QuicStreamType.Unidirectional);
    // The following call will get suspended because the stream's limit has been reached.
    var taskStream3 = connection.OpenOutboundStreamAsync(QuicStreamType.Unidirectional);
    
  6. The client finishes and closes the first two streams:

    await stream1.WriteAsync(data, completeWrites: true);
    await stream1.DisposeAsync();
    await stream2.WriteAsync(data, completeWrites: true);
    await stream2.DisposeAsync();
    Console.WriteLine($"Stream 3 {(taskStream3.IsCompleted ? "opened" : "pending")}");
    
  7. The client prints:

    Stream 3 pending
    
  8. The server releases additional capacity of 2 after processing the first two streams.

  9. Two things happen on the client. First, a third stream is opened:

    var stream3 = await taskStream3;
    

    Then, the client's StreamCapacityCallback is called again and prints:

    [conn][0x58575BF805B0] stream capacity increased by: unidi += 2, bidi += 0
    

This property is optional.

QuicServerConnectionOptions

QuicServerConnectionOptions options are specific for a server-side connection. Apart from inherited properties from QuicConnectionOptions, it contains the following:

ServerAuthenticationOptions

ServerAuthenticationOptions contains TLS settings for the server connection. The options are the same as used in SslStream.AuthenticateAsServer(SslServerAuthenticationOptions) and SslStream.AuthenticateAsServerAsync(SslServerAuthenticationOptions, CancellationToken). For the QUIC server, SslServerAuthenticationOptions is valid if:

This property is mandatory and must meet the listed conditions.

QuicClientConnectionOptions

QuicClientConnectionOptions options are specific to a client-side connection. Apart from inherited properties from QuicConnectionOptions, it contains the following:

ClientAuthenticationOptions

ClientAuthenticationOptions contains the TLS setting for the client connection. The options are the same as used in SslStream.AuthenticateAsClient(SslClientAuthenticationOptions) and SslStream.AuthenticateAsClientAsync(SslClientAuthenticationOptions, CancellationToken). For the QUIC client, SslClientAuthenticationOptions is valid if:

This property is mandatory and must meet the listed conditions.

LocalEndPoint

LocalEndPoint contains the IP address and port to which the client connection will bind. If not specified, the OS assigns an IP address and a port. This property is optional.

RemoteEndPoint

RemoteEndPoint can either be DnsEndPoint or IPEndPoint of the peer to which the connection is being established. If it's a DnsEndPoint, the first IP address returned by Dns.GetHostAddressesAsync(String, CancellationToken) is used. This property is mandatory.