File: System\Net\Quic\QuicConnection.cs
Web Access
Project: src\src\libraries\System.Net.Quic\src\System.Net.Quic.csproj (System.Net.Quic)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Net.Security;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using Microsoft.Quic;
using static Microsoft.Quic.MsQuic;
using CONNECTED_DATA = Microsoft.Quic.QUIC_CONNECTION_EVENT._Anonymous_e__Union._CONNECTED_e__Struct;
using LOCAL_ADDRESS_CHANGED_DATA = Microsoft.Quic.QUIC_CONNECTION_EVENT._Anonymous_e__Union._LOCAL_ADDRESS_CHANGED_e__Struct;
using PEER_ADDRESS_CHANGED_DATA = Microsoft.Quic.QUIC_CONNECTION_EVENT._Anonymous_e__Union._PEER_ADDRESS_CHANGED_e__Struct;
using PEER_CERTIFICATE_RECEIVED_DATA = Microsoft.Quic.QUIC_CONNECTION_EVENT._Anonymous_e__Union._PEER_CERTIFICATE_RECEIVED_e__Struct;
using PEER_STREAM_STARTED_DATA = Microsoft.Quic.QUIC_CONNECTION_EVENT._Anonymous_e__Union._PEER_STREAM_STARTED_e__Struct;
using STREAMS_AVAILABLE_DATA = Microsoft.Quic.QUIC_CONNECTION_EVENT._Anonymous_e__Union._STREAMS_AVAILABLE_e__Struct;
using SHUTDOWN_COMPLETE_DATA = Microsoft.Quic.QUIC_CONNECTION_EVENT._Anonymous_e__Union._SHUTDOWN_COMPLETE_e__Struct;
using SHUTDOWN_INITIATED_BY_PEER_DATA = Microsoft.Quic.QUIC_CONNECTION_EVENT._Anonymous_e__Union._SHUTDOWN_INITIATED_BY_PEER_e__Struct;
using SHUTDOWN_INITIATED_BY_TRANSPORT_DATA = Microsoft.Quic.QUIC_CONNECTION_EVENT._Anonymous_e__Union._SHUTDOWN_INITIATED_BY_TRANSPORT_e__Struct;
 
namespace System.Net.Quic;
 
/// <summary>
/// Represents a QUIC connection, see <see href="https://www.rfc-editor.org/rfc/rfc9000.html#name-connections">RFC 9000: Connections</see> for more details.
/// <see cref="QuicConnection" /> itself doesn't send or receive data but rather allows opening and/or accepting multiple <see cref="QuicStream" />.
/// </summary>
/// <remarks>
/// <see cref="QuicConnection" /> can either be accepted from <see cref="QuicListener.AcceptConnectionAsync(CancellationToken)" /> (inbound connection),
/// or create with a static method <see cref="QuicConnection.ConnectAsync(System.Net.Quic.QuicClientConnectionOptions, CancellationToken)" /> (outbound connection).
///
/// Each connection can then open outbound stream: <see cref="QuicConnection.OpenOutboundStreamAsync(QuicStreamType, CancellationToken)" />,
/// or accept an inbound stream: <see cref="QuicConnection.AcceptInboundStreamAsync(CancellationToken)" />.
/// </remarks>
public sealed partial class QuicConnection : IAsyncDisposable
{
    /// <summary>
    /// Returns <c>true</c> if QUIC is supported on the current machine and can be used; otherwise, <c>false</c>.
    /// </summary>
    /// <remarks>
    /// The current implementation depends on <see href="https://github.com/microsoft/msquic">MsQuic</see> native library, this property checks its presence (Linux machines).
    /// It also checks whether TLS 1.3, requirement for QUIC protocol, is available and enabled (Windows machines).
    /// </remarks>
    [SupportedOSPlatformGuard("windows")]
    [SupportedOSPlatformGuard("linux")]
    [SupportedOSPlatformGuard("osx")]
    public static bool IsSupported => MsQuicApi.IsQuicSupported;
 
    /// <summary>
    /// Creates a new <see cref="QuicConnection"/> and connects it to the peer.
    /// </summary>
    /// <param name="options">Options for the connection.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
    /// <returns>An asynchronous task that completes with the connected connection.</returns>
    public static ValueTask<QuicConnection> ConnectAsync(QuicClientConnectionOptions options, CancellationToken cancellationToken = default)
    {
        if (!IsSupported)
        {
            throw new PlatformNotSupportedException(SR.Format(SR.SystemNetQuic_PlatformNotSupported, MsQuicApi.NotSupportedReason ?? "General loading failure."));
        }
 
        // Validate and fill in defaults for the options.
        options.Validate(nameof(options));
        return StartConnectAsync(options, cancellationToken);
 
        static async ValueTask<QuicConnection> StartConnectAsync(QuicClientConnectionOptions options, CancellationToken cancellationToken)
        {
            QuicConnection connection = new QuicConnection();
 
            using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
 
            if (options.HandshakeTimeout != Timeout.InfiniteTimeSpan && options.HandshakeTimeout != TimeSpan.Zero)
            {
                linkedCts.CancelAfter(options.HandshakeTimeout);
            }
 
            try
            {
                await connection.FinishConnectAsync(options, linkedCts.Token).ConfigureAwait(false);
            }
            catch (OperationCanceledException)
            {
                await connection.DisposeAsync().ConfigureAwait(false);
 
                // Throw OCE with correct token if cancellation requested by user.
                cancellationToken.ThrowIfCancellationRequested();
 
                // Cancellation by the linkedCts.CancelAfter, convert to timeout.
                throw new QuicException(QuicError.ConnectionTimeout, null, SR.Format(SR.net_quic_handshake_timeout, options.HandshakeTimeout));
            }
            catch
            {
                await connection.DisposeAsync().ConfigureAwait(false);
                throw;
            }
 
            return connection;
        }
    }
 
    /// <summary>
    /// Handle to MsQuic connection object.
    /// </summary>
    private readonly MsQuicContextSafeHandle _handle;
 
    /// <summary>
    /// Set to true once disposed. Prevents double and/or concurrent disposal.
    /// </summary>
    private bool _disposed;
 
    private readonly ValueTaskSource _connectedTcs = new ValueTaskSource();
    private readonly ResettableValueTaskSource _shutdownTcs = new ResettableValueTaskSource()
    {
        CancellationAction = target =>
        {
            try
            {
                if (target is QuicConnection connection)
                {
                    // The OCE will be propagated through stored CancellationToken in ResettableValueTaskSource.
                    connection._shutdownTcs.TrySetResult();
                }
            }
            catch (ObjectDisposedException)
            {
                // We collided with a Dispose in another thread. This can happen
                // when using CancellationTokenSource.CancelAfter.
                // Ignore the exception
            }
        }
    };
 
    /// <summary>
    /// Completed when connection shutdown is initiated.
    /// </summary>
    private readonly TaskCompletionSource _connectionCloseTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
 
    private readonly CancellationTokenSource _shutdownTokenSource = new CancellationTokenSource();
 
    // Token that fires when the connection is closed.
    internal CancellationToken ConnectionShutdownToken => _shutdownTokenSource.Token;
 
    private readonly Channel<QuicStream> _acceptQueue = Channel.CreateUnbounded<QuicStream>(new UnboundedChannelOptions()
    {
        SingleWriter = true
    });
 
    /// <summary>
    /// Holds options to validate peer certificate.
    /// Set up either in <see cref="FinishHandshakeAsync"/> for an inbound connection or in <see cref="FinishConnectAsync"/> for an outbound.
    /// </summary>
    private SslConnectionOptions _sslConnectionOptions;
    /// <summary>
    /// Holds MsQuic connection configuration.
    /// Set up either in <see cref="FinishHandshakeAsync"/> for an inbound connection or in <see cref="FinishConnectAsync"/> for an outbound.
    /// </summary>
    private MsQuicSafeHandle? _configuration;
 
    /// <summary>
    /// Used by <see cref="AcceptInboundStreamAsync(CancellationToken)" /> to throw in case no stream can be opened from the peer.
    /// <c>true</c> when at least one of <see cref="QuicConnectionOptions.MaxInboundBidirectionalStreams" /> or <see cref="QuicConnectionOptions.MaxInboundUnidirectionalStreams" /> is greater than <c>0</c>.
    /// </summary>
    private bool _canAccept;
    /// <summary>
    /// From <see cref="QuicConnectionOptions.DefaultStreamErrorCode"/>, passed to newly created <see cref="QuicStream"/>.
    /// </summary>
    private long _defaultStreamErrorCode;
    /// <summary>
    /// From <see cref="QuicConnectionOptions.DefaultCloseErrorCode"/>, used to close connection in <see cref="DisposeAsync"/>.
    /// </summary>
    private long _defaultCloseErrorCode;
 
    /// <summary>
    /// Set when CONNECTED is received or inside the constructor for an inbound connection from NEW_CONNECTION data.
    /// </summary>
    private IPEndPoint _remoteEndPoint = null!;
    /// <summary>
    /// Set when CONNECTED is received or inside the constructor for an inbound connection from NEW_CONNECTION data.
    /// </summary>
    private IPEndPoint _localEndPoint = null!;
    /// <summary>
    /// Occurres when an additional stream capacity has been released by the peer. Corresponds to receiving a MAX_STREAMS frame.
    /// </summary>
    private Action<QuicConnection, QuicStreamCapacityChangedArgs>? _streamCapacityCallback;
    /// <summary>
    /// Optimization to avoid `Action` instantiation with every <see cref="OpenOutboundStreamAsync(QuicStreamType, CancellationToken)"/>.
    /// Holds <see cref="DecrementStreamCapacity(QuicStreamType)"/> method.
    /// </summary>
    private Action<QuicStreamType> _decrementStreamCapacity;
    /// <summary>
    /// Represents how many bidirectional streams can be accepted by the peer. Is only manipulated from MsQuic thread.
    /// </summary>
    private int _bidirectionalStreamCapacity;
    /// <summary>
    /// Represents how many unidirectional streams can be accepted by the peer. Is only manipulated from MsQuic thread.
    /// </summary>
    private int _unidirectionalStreamCapacity;
    /// <summary>
    /// Keeps track whether <see cref="RemoteCertificate"/> has been accessed so that we know whether to dispose the certificate or not.
    /// </summary>
    private bool _remoteCertificateExposed;
    /// <summary>
    /// Set when PEER_CERTIFICATE_RECEIVED is received (before CONNECTED).
    /// For an outbound/client connection will always have the peer's (server) certificate; for an inbound/server one, only if the connection requested and the peer (client) provided one.
    /// </summary>
    private X509Certificate2? _remoteCertificate;
    /// <summary>
    /// Set when CONNECTED is received.
    /// </summary>
    private SslApplicationProtocol _negotiatedApplicationProtocol;
    /// <summary>
    /// Set when CONNECTED is received.
    /// </summary>
    private TlsCipherSuite _negotiatedCipherSuite;
    /// <summary>
    /// Set when CONNECTED is received.
    /// </summary>
    private SslProtocols _negotiatedSslProtocol;
 
    /// <summary>
    /// Will contain TLS secret after CONNECTED event is received and store it into SSLKEYLOGFILE.
    /// MsQuic holds the underlying pointer so this object can be disposed only after connection native handle gets closed.
    /// </summary>
    private readonly MsQuicTlsSecret? _tlsSecret;
 
    /// <summary>
    /// The remote endpoint used for this connection.
    /// </summary>
    public IPEndPoint RemoteEndPoint => _remoteEndPoint;
    /// <summary>
    /// The local endpoint used for this connection.
    /// </summary>
    public IPEndPoint LocalEndPoint => _localEndPoint;
 
    private async void OnStreamCapacityIncreased(int bidirectionalIncrement, int unidirectionalIncrement)
    {
        // Bail out early to avoid queueing work on the thread pool as well as event args instantiation.
        if (_streamCapacityCallback is null)
        {
            return;
        }
        // No increment, nothing to report.
        if (bidirectionalIncrement == 0 && unidirectionalIncrement == 0)
        {
            return;
        }
 
        // Do not invoke user-defined event handler code on MsQuic thread.
        await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
 
        try
        {
            if (NetEventSource.Log.IsEnabled())
            {
                NetEventSource.Info(this, $"{this} Signaling StreamCapacityIncreased with {bidirectionalIncrement} bidirectional increment (absolute value {_bidirectionalStreamCapacity}) and {unidirectionalIncrement} unidirectional increment (absolute value {_unidirectionalStreamCapacity}).");
            }
            _streamCapacityCallback(this, new QuicStreamCapacityChangedArgs { BidirectionalIncrement = bidirectionalIncrement, UnidirectionalIncrement = unidirectionalIncrement });
        }
        catch (Exception ex)
        {
            // Just log the exception, we're on a thread-pool thread and there's no way to report this to anyone.
            if (NetEventSource.Log.IsEnabled())
            {
                NetEventSource.Info(this, $"{this} {nameof(QuicConnectionOptions.StreamCapacityCallback)} failed with {ex}.");
            }
        }
    }
 
    /// <summary>
    /// Gets the name of the server the client is trying to connect to. That name is used for server certificate validation. It can be a DNS name or an IP address.
    /// </summary>
    /// <returns>The name of the server the client is trying to connect to.</returns>
    public string TargetHostName => _sslConnectionOptions.TargetHost;
 
    /// <summary>
    /// The certificate provided by the peer.
    /// For an outbound/client connection will always have the peer's (server) certificate; for an inbound/server one, only if the connection requested and the peer (client) provided one.
    /// </summary>
    public X509Certificate? RemoteCertificate
    {
        get
        {
            _remoteCertificateExposed = true;
            return _remoteCertificate;
        }
    }
 
    /// <summary>
    /// Final, negotiated application protocol.
    /// </summary>
    public SslApplicationProtocol NegotiatedApplicationProtocol => _negotiatedApplicationProtocol;
 
    /// <summary>
    /// Gets the cipher suite which was negotiated for this connection.
    /// </summary>
    [CLSCompliant(false)]
    public TlsCipherSuite NegotiatedCipherSuite => _negotiatedCipherSuite;
 
    /// <summary>
    /// Gets a <see cref="System.Security.Authentication.SslProtocols"/> value that indicates the security protocol used to authenticate this connection.
    /// </summary>
    public SslProtocols SslProtocol => _negotiatedSslProtocol;
 
    /// <inheritdoc />
    public override string ToString() => _handle.ToString();
 
    /// <summary>
    /// Initializes a new instance of an outbound <see cref="QuicConnection" />.
    /// </summary>
    private unsafe QuicConnection()
    {
        GCHandle context = GCHandle.Alloc(this, GCHandleType.Weak);
        try
        {
            QUIC_HANDLE* handle;
            ThrowHelper.ThrowIfMsQuicError(MsQuicApi.Api.ConnectionOpen(
                MsQuicApi.Api.Registration,
                &NativeCallback,
                (void*)GCHandle.ToIntPtr(context),
                &handle),
                "ConnectionOpen failed");
            _handle = new MsQuicContextSafeHandle(handle, context, SafeHandleType.Connection);
        }
        catch
        {
            context.Free();
            throw;
        }
 
        if (NetEventSource.Log.IsEnabled())
        {
            NetEventSource.Info(this, $"{this} New outbound connection.");
        }
 
        _decrementStreamCapacity = DecrementStreamCapacity;
        _tlsSecret = MsQuicTlsSecret.Create(_handle);
    }
 
    /// <summary>
    /// Initializes a new instance of an inbound <see cref="QuicConnection" />.
    /// </summary>
    /// <param name="handle">Native handle.</param>
    /// <param name="info">Related data from the NEW_CONNECTION listener event.</param>
    internal unsafe QuicConnection(QUIC_HANDLE* handle, QUIC_NEW_CONNECTION_INFO* info)
    {
        GCHandle context = GCHandle.Alloc(this, GCHandleType.Weak);
        try
        {
            _handle = new MsQuicContextSafeHandle(handle, context, SafeHandleType.Connection);
            delegate* unmanaged[Cdecl]<QUIC_HANDLE*, void*, QUIC_CONNECTION_EVENT*, int> nativeCallback = &NativeCallback;
            MsQuicApi.Api.SetCallbackHandler(
                _handle,
                nativeCallback,
                (void*)GCHandle.ToIntPtr(context));
        }
        catch
        {
            context.Free();
            throw;
        }
 
        _remoteEndPoint = MsQuicHelpers.QuicAddrToIPEndPoint(info->RemoteAddress);
        _localEndPoint = MsQuicHelpers.QuicAddrToIPEndPoint(info->LocalAddress);
        _decrementStreamCapacity = DecrementStreamCapacity;
        _tlsSecret = MsQuicTlsSecret.Create(_handle);
    }
 
    private async ValueTask FinishConnectAsync(QuicClientConnectionOptions options, CancellationToken cancellationToken = default)
    {
        if (_connectedTcs.TryInitialize(out ValueTask valueTask, this, cancellationToken))
        {
            _canAccept = options.MaxInboundBidirectionalStreams > 0 || options.MaxInboundUnidirectionalStreams > 0;
            _defaultStreamErrorCode = options.DefaultStreamErrorCode;
            _defaultCloseErrorCode = options.DefaultCloseErrorCode;
            _streamCapacityCallback = options.StreamCapacityCallback;
 
            if (!options.RemoteEndPoint.TryParse(out string? host, out IPAddress? address, out int port))
            {
                throw new ArgumentException(SR.Format(SR.net_quic_unsupported_endpoint_type, options.RemoteEndPoint.GetType()), nameof(options));
            }
 
            if (address is null)
            {
                Debug.Assert(host is not null);
 
                // Given just a ServerName to connect to, MsQuic would also use the first address after the resolution
                // (https://github.com/microsoft/msquic/issues/1181) and it would not return a well-known error code
                // for resolution failures we could rely on. By doing the resolution in managed code, we can guarantee
                // that a SocketException will surface to the user if the name resolution fails.
                IPAddress[] addresses = await Dns.GetHostAddressesAsync(host, cancellationToken).ConfigureAwait(false);
                cancellationToken.ThrowIfCancellationRequested();
                if (addresses.Length == 0)
                {
                    throw new SocketException((int)SocketError.HostNotFound);
                }
                address = addresses[0];
            }
 
            QuicAddr remoteQuicAddress = new IPEndPoint(address, port).ToQuicAddr();
            MsQuicHelpers.SetMsQuicParameter(_handle, QUIC_PARAM_CONN_REMOTE_ADDRESS, remoteQuicAddress);
 
            if (options.LocalEndPoint is not null)
            {
                QuicAddr localQuicAddress = options.LocalEndPoint.ToQuicAddr();
                MsQuicHelpers.SetMsQuicParameter(_handle, QUIC_PARAM_CONN_LOCAL_ADDRESS, localQuicAddress);
            }
 
            _sslConnectionOptions = new SslConnectionOptions(
                this,
                isClient: true,
                options.ClientAuthenticationOptions.TargetHost ?? host ?? address.ToString(),
                certificateRequired: true,
                options.ClientAuthenticationOptions.CertificateRevocationCheckMode,
                options.ClientAuthenticationOptions.RemoteCertificateValidationCallback,
                options.ClientAuthenticationOptions.CertificateChainPolicy?.Clone());
            _configuration = MsQuicConfiguration.Create(options);
 
            // RFC 6066 forbids IP literals.
            // IDN mapping is handled by MsQuic.
            string sni = (TargetHostNameHelper.IsValidAddress(options.ClientAuthenticationOptions.TargetHost) ? null : options.ClientAuthenticationOptions.TargetHost) ?? host ?? string.Empty;
 
            IntPtr targetHostPtr = Marshal.StringToCoTaskMemUTF8(sni);
            try
            {
                unsafe
                {
                    ThrowHelper.ThrowIfMsQuicError(MsQuicApi.Api.ConnectionStart(
                        _handle,
                        _configuration,
                        (ushort)remoteQuicAddress.Family,
                        (sbyte*)targetHostPtr,
                        (ushort)port),
                        "ConnectionStart failed");
                }
            }
            finally
            {
                Marshal.FreeCoTaskMem(targetHostPtr);
            }
        }
 
        await valueTask.ConfigureAwait(false);
    }
 
    internal ValueTask FinishHandshakeAsync(QuicServerConnectionOptions options, string targetHost, CancellationToken cancellationToken = default)
    {
        if (_connectedTcs.TryInitialize(out ValueTask valueTask, this, cancellationToken))
        {
            _canAccept = options.MaxInboundBidirectionalStreams > 0 || options.MaxInboundUnidirectionalStreams > 0;
            _defaultStreamErrorCode = options.DefaultStreamErrorCode;
            _defaultCloseErrorCode = options.DefaultCloseErrorCode;
            _streamCapacityCallback = options.StreamCapacityCallback;
 
            // RFC 6066 forbids IP literals, avoid setting IP address here for consistency with SslStream
            if (TargetHostNameHelper.IsValidAddress(targetHost))
            {
                targetHost = string.Empty;
            }
 
            _sslConnectionOptions = new SslConnectionOptions(
                this,
                isClient: false,
                targetHost,
                options.ServerAuthenticationOptions.ClientCertificateRequired,
                options.ServerAuthenticationOptions.CertificateRevocationCheckMode,
                options.ServerAuthenticationOptions.RemoteCertificateValidationCallback,
                options.ServerAuthenticationOptions.CertificateChainPolicy?.Clone());
            _configuration = MsQuicConfiguration.Create(options, targetHost);
 
            unsafe
            {
                ThrowHelper.ThrowIfMsQuicError(MsQuicApi.Api.ConnectionSetConfiguration(
                    _handle,
                    _configuration),
                    "ConnectionSetConfiguration failed");
            }
        }
 
        return valueTask;
    }
 
    /// <summary>
    /// In order to provide meaningful increments in <see cref="_streamCapacityCallback"/>, available streams count can be only manipulated from MsQuic thread.
    /// For that purpose we pass this function to <see cref="QuicStream"/> so that it can call it from <c>START_COMPLETE</c> event handler.
    ///
    /// Note that MsQuic itself manipulates stream counts right before indicating <c>START_COMPLETE</c> event.
    /// </summary>
    /// <param name="streamType">Type of the stream to decrement appropriate field.</param>
    private void DecrementStreamCapacity(QuicStreamType streamType)
    {
        if (streamType == QuicStreamType.Unidirectional)
        {
            --_unidirectionalStreamCapacity;
            if (NetEventSource.Log.IsEnabled())
            {
                NetEventSource.Info(this, $"{this} decremented stream count for {streamType} to {_unidirectionalStreamCapacity}.");
            }
        }
        if (streamType == QuicStreamType.Bidirectional)
        {
            --_bidirectionalStreamCapacity;
            if (NetEventSource.Log.IsEnabled())
            {
                NetEventSource.Info(this, $"{this} decremented stream count for {streamType} to {_bidirectionalStreamCapacity}.");
            }
        }
    }
 
    /// <summary>
    /// Create an outbound uni/bidirectional <see cref="QuicStream" />.
    /// In case the connection doesn't have any available stream capacity, i.e.: the peer limits the concurrent stream count,
    /// the operation will pend until the stream can be opened (other stream gets closed or peer increases the stream limit).
    /// </summary>
    /// <param name="type">The type of the stream, i.e. unidirectional or bidirectional.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
    /// <returns>An asynchronous task that completes with the opened <see cref="QuicStream" />.</returns>
    public async ValueTask<QuicStream> OpenOutboundStreamAsync(QuicStreamType type, CancellationToken cancellationToken = default)
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
 
        QuicStream? stream = null;
        try
        {
            stream = new QuicStream(_handle, type, _defaultStreamErrorCode);
 
            if (NetEventSource.Log.IsEnabled())
            {
                NetEventSource.Info(this, $"{this} New outbound {type} stream {stream}.");
            }
 
            await stream.StartAsync(_decrementStreamCapacity, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            if (stream is not null)
            {
                await stream.DisposeAsync().ConfigureAwait(false);
            }
 
            // Propagate ODE if disposed in the meantime.
            ObjectDisposedException.ThrowIf(_disposed, this);
 
            // Propagate connection error when the connection was closed (remotely = ABORTED / locally = INVALID_STATE).
            if (ex is QuicException qex && qex.QuicError == QuicError.InternalError &&
               (qex.HResult == QUIC_STATUS_ABORTED || qex.HResult == QUIC_STATUS_INVALID_STATE))
            {
                await _connectionCloseTcs.Task.ConfigureAwait(false);
            }
            throw;
        }
        return stream;
    }
 
    /// <summary>
    /// Accepts an inbound <see cref="QuicStream" />.
    /// </summary>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
    /// <returns>An asynchronous task that completes with the accepted <see cref="QuicStream" />.</returns>
    public async ValueTask<QuicStream> AcceptInboundStreamAsync(CancellationToken cancellationToken = default)
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
 
        if (!_canAccept)
        {
            throw new InvalidOperationException(SR.net_quic_accept_not_allowed);
        }
 
        GCHandle keepObject = GCHandle.Alloc(this);
        try
        {
            return await _acceptQueue.Reader.ReadAsync(cancellationToken).ConfigureAwait(false);
        }
        catch (ChannelClosedException ex) when (ex.InnerException is not null)
        {
            ExceptionDispatchInfo.Throw(ex.InnerException);
            throw;
        }
        finally
        {
            keepObject.Free();
        }
    }
 
    /// <summary>
    /// Closes the connection with the application provided code, see <see href="https://www.rfc-editor.org/rfc/rfc9000.html#immediate-close">RFC 9000: Connection Termination</see> for more details.
    /// </summary>
    /// <remarks>
    /// Connection close is not graceful in regards to its streams, i.e.: calling <see cref="CloseAsync(long, CancellationToken)"/> will immediately close all streams associated with this connection.
    /// Make sure, that all streams have been closed and all their data consumed before calling this method;
    /// otherwise, all the data that were received but not consumed yet, will be lost.
    ///
    /// If <see cref="CloseAsync(long, CancellationToken)"/> is not called before <see cref="DisposeAsync">disposing</see> the connection,
    /// the <see cref="QuicConnectionOptions.DefaultCloseErrorCode"/> will be used by <see cref="DisposeAsync"/> to close the connection.
    /// </remarks>
    /// <param name="errorCode">Application provided code with the reason for closure.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
    /// <returns>An asynchronous task that completes when the connection is closed.</returns>
    public ValueTask CloseAsync(long errorCode, CancellationToken cancellationToken = default)
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
        ThrowHelper.ValidateErrorCode(nameof(errorCode), errorCode, $"{nameof(CloseAsync)}.{nameof(errorCode)}");
 
        if (_shutdownTcs.TryGetValueTask(out ValueTask valueTask, this, cancellationToken))
        {
            if (NetEventSource.Log.IsEnabled())
            {
                NetEventSource.Info(this, $"{this} Closing connection, Error code = {errorCode}");
            }
 
            unsafe
            {
                MsQuicApi.Api.ConnectionShutdown(
                    _handle,
                    QUIC_CONNECTION_SHUTDOWN_FLAGS.NONE,
                    (ulong)errorCode);
            }
        }
 
        return valueTask;
    }
 
    private unsafe int HandleEventConnected(ref CONNECTED_DATA data)
    {
        _negotiatedApplicationProtocol = new SslApplicationProtocol(new Span<byte>(data.NegotiatedAlpn, data.NegotiatedAlpnLength).ToArray());
 
        QUIC_HANDSHAKE_INFO info = MsQuicHelpers.GetMsQuicParameter<QUIC_HANDSHAKE_INFO>(_handle, QUIC_PARAM_TLS_HANDSHAKE_INFO);
 
        // QUIC_CIPHER_SUITE and QUIC_TLS_PROTOCOL_VERSION use the same values as the corresponding TlsCipherSuite and SslProtocols members.
        _negotiatedCipherSuite = (TlsCipherSuite)info.CipherSuite;
        _negotiatedSslProtocol = (SslProtocols)info.TlsProtocolVersion;
 
        // currently only TLS 1.3 is defined for QUIC
        Debug.Assert(_negotiatedSslProtocol == SslProtocols.Tls13, $"Unexpected TLS version {info.TlsProtocolVersion}");
 
        QuicAddr remoteAddress = MsQuicHelpers.GetMsQuicParameter<QuicAddr>(_handle, QUIC_PARAM_CONN_REMOTE_ADDRESS);
        _remoteEndPoint = MsQuicHelpers.QuicAddrToIPEndPoint(&remoteAddress);
 
        QuicAddr localAddress = MsQuicHelpers.GetMsQuicParameter<QuicAddr>(_handle, QUIC_PARAM_CONN_LOCAL_ADDRESS);
        _localEndPoint = MsQuicHelpers.QuicAddrToIPEndPoint(&localAddress);
 
        // Final (1-RTT) secrets have been derived, log them if desired to allow decrypting application traffic.
        _tlsSecret?.WriteSecret();
 
        if (NetEventSource.Log.IsEnabled())
        {
            NetEventSource.Info(this, $"{this} Connection connected {LocalEndPoint} -> {RemoteEndPoint} for {_negotiatedApplicationProtocol} protocol");
        }
        _connectedTcs.TrySetResult();
        return QUIC_STATUS_SUCCESS;
    }
    private unsafe int HandleEventShutdownInitiatedByTransport(ref SHUTDOWN_INITIATED_BY_TRANSPORT_DATA data)
    {
        Exception exception = ExceptionDispatchInfo.SetCurrentStackTrace(ThrowHelper.GetExceptionForMsQuicStatus(data.Status, (long)data.ErrorCode));
        _connectedTcs.TrySetException(exception);
        _connectionCloseTcs.TrySetException(exception);
        _acceptQueue.Writer.TryComplete(exception);
        return QUIC_STATUS_SUCCESS;
    }
    private unsafe int HandleEventShutdownInitiatedByPeer(ref SHUTDOWN_INITIATED_BY_PEER_DATA data)
    {
        Exception exception = ExceptionDispatchInfo.SetCurrentStackTrace(ThrowHelper.GetConnectionAbortedException((long)data.ErrorCode));
        _connectionCloseTcs.TrySetException(exception);
        _acceptQueue.Writer.TryComplete(exception);
        return QUIC_STATUS_SUCCESS;
    }
    private unsafe int HandleEventShutdownComplete()
    {
        // make sure we log at least some secrets in case of shutdown before handshake completes.
        _tlsSecret?.WriteSecret();
 
        Exception exception = ExceptionDispatchInfo.SetCurrentStackTrace(_disposed ? new ObjectDisposedException(GetType().FullName) : ThrowHelper.GetOperationAbortedException());
        _connectionCloseTcs.TrySetException(exception);
        _acceptQueue.Writer.TryComplete(exception);
        _connectedTcs.TrySetException(exception);
        _shutdownTokenSource.Cancel();
        _shutdownTcs.TrySetResult(final: true);
        return QUIC_STATUS_SUCCESS;
    }
    private unsafe int HandleEventLocalAddressChanged(ref LOCAL_ADDRESS_CHANGED_DATA data)
    {
        _localEndPoint = MsQuicHelpers.QuicAddrToIPEndPoint(data.Address);
        return QUIC_STATUS_SUCCESS;
    }
    private unsafe int HandleEventPeerAddressChanged(ref PEER_ADDRESS_CHANGED_DATA data)
    {
        _remoteEndPoint = MsQuicHelpers.QuicAddrToIPEndPoint(data.Address);
        return QUIC_STATUS_SUCCESS;
    }
    private unsafe int HandleEventPeerStreamStarted(ref PEER_STREAM_STARTED_DATA data)
    {
        QuicStream stream = new QuicStream(_handle, data.Stream, data.Flags, _defaultStreamErrorCode);
 
        if (NetEventSource.Log.IsEnabled())
        {
            QuicStreamType type = data.Flags.HasFlag(QUIC_STREAM_OPEN_FLAGS.UNIDIRECTIONAL) ? QuicStreamType.Unidirectional : QuicStreamType.Bidirectional;
            NetEventSource.Info(this, $"{this} New inbound {type} stream {stream}, Id = {stream.Id}.");
        }
 
        if (!_acceptQueue.Writer.TryWrite(stream))
        {
            if (NetEventSource.Log.IsEnabled())
            {
                NetEventSource.Error(this, $"{this} Unable to enqueue incoming stream {stream}");
            }
 
            stream.Dispose();
            return QUIC_STATUS_SUCCESS;
        }
 
        data.Flags |= QUIC_STREAM_OPEN_FLAGS.DELAY_ID_FC_UPDATES;
        return QUIC_STATUS_SUCCESS;
    }
    private unsafe int HandleEventStreamsAvailable(ref STREAMS_AVAILABLE_DATA data)
    {
        int bidirectionalIncrement = 0;
        int unidirectionalIncrement = 0;
        if (data.BidirectionalCount > 0)
        {
            bidirectionalIncrement = data.BidirectionalCount - _bidirectionalStreamCapacity;
            _bidirectionalStreamCapacity = data.BidirectionalCount;
        }
        if (data.UnidirectionalCount > 0)
        {
            unidirectionalIncrement = data.UnidirectionalCount - _unidirectionalStreamCapacity;
            _unidirectionalStreamCapacity = data.UnidirectionalCount;
        }
        OnStreamCapacityIncreased(bidirectionalIncrement, unidirectionalIncrement);
        return QUIC_STATUS_SUCCESS;
    }
    private unsafe int HandleEventPeerCertificateReceived(ref PEER_CERTIFICATE_RECEIVED_DATA data)
    {
        //
        // The certificate validation is an expensive operation and we don't want to delay MsQuic
        // worker thread. So we offload the validation to the .NET thread pool. Incidentally, this
        // also prevents potential user RemoteCertificateValidationCallback from blocking MsQuic
        // worker threads.
        //
 
        // Handshake keys should be available by now, log them now if desired.
        _tlsSecret?.WriteSecret();
 
        var task = _sslConnectionOptions.StartAsyncCertificateValidation((IntPtr)data.Certificate, (IntPtr)data.Chain);
        if (task.IsCompletedSuccessfully)
        {
            return task.Result ? QUIC_STATUS_SUCCESS : QUIC_STATUS_BAD_CERTIFICATE;
        }
 
        return QUIC_STATUS_PENDING;
    }
 
    private unsafe int HandleConnectionEvent(ref QUIC_CONNECTION_EVENT connectionEvent)
        => connectionEvent.Type switch
        {
            QUIC_CONNECTION_EVENT_TYPE.CONNECTED => HandleEventConnected(ref connectionEvent.CONNECTED),
            QUIC_CONNECTION_EVENT_TYPE.SHUTDOWN_INITIATED_BY_TRANSPORT => HandleEventShutdownInitiatedByTransport(ref connectionEvent.SHUTDOWN_INITIATED_BY_TRANSPORT),
            QUIC_CONNECTION_EVENT_TYPE.SHUTDOWN_INITIATED_BY_PEER => HandleEventShutdownInitiatedByPeer(ref connectionEvent.SHUTDOWN_INITIATED_BY_PEER),
            QUIC_CONNECTION_EVENT_TYPE.SHUTDOWN_COMPLETE => HandleEventShutdownComplete(),
            QUIC_CONNECTION_EVENT_TYPE.LOCAL_ADDRESS_CHANGED => HandleEventLocalAddressChanged(ref connectionEvent.LOCAL_ADDRESS_CHANGED),
            QUIC_CONNECTION_EVENT_TYPE.PEER_ADDRESS_CHANGED => HandleEventPeerAddressChanged(ref connectionEvent.PEER_ADDRESS_CHANGED),
            QUIC_CONNECTION_EVENT_TYPE.PEER_STREAM_STARTED => HandleEventPeerStreamStarted(ref connectionEvent.PEER_STREAM_STARTED),
            QUIC_CONNECTION_EVENT_TYPE.STREAMS_AVAILABLE => HandleEventStreamsAvailable(ref connectionEvent.STREAMS_AVAILABLE),
            QUIC_CONNECTION_EVENT_TYPE.PEER_CERTIFICATE_RECEIVED => HandleEventPeerCertificateReceived(ref connectionEvent.PEER_CERTIFICATE_RECEIVED),
            _ => QUIC_STATUS_SUCCESS,
        };
 
#pragma warning disable CS3016
    [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })]
#pragma warning restore CS3016
    private static unsafe int NativeCallback(QUIC_HANDLE* connection, void* context, QUIC_CONNECTION_EVENT* connectionEvent)
    {
        GCHandle stateHandle = GCHandle.FromIntPtr((IntPtr)context);
 
        // Check if the instance hasn't been collected.
        if (!stateHandle.IsAllocated || stateHandle.Target is not QuicConnection instance)
        {
            if (NetEventSource.Log.IsEnabled())
            {
                NetEventSource.Error(null, $"Received event {connectionEvent->Type} for [conn][{(nint)connection:X11}] while connection is already disposed");
            }
            return QUIC_STATUS_INVALID_STATE;
        }
 
        try
        {
            // Process the event.
            if (NetEventSource.Log.IsEnabled())
            {
                NetEventSource.Info(instance, $"{instance} Received event {connectionEvent->Type} {connectionEvent->ToString()}");
            }
            return instance.HandleConnectionEvent(ref *connectionEvent);
        }
        catch (Exception ex)
        {
            if (NetEventSource.Log.IsEnabled())
            {
                NetEventSource.Error(instance, $"{instance} Exception while processing event {connectionEvent->Type}: {ex}");
            }
            return QUIC_STATUS_INTERNAL_ERROR;
        }
    }
 
    /// <summary>
    /// If not closed explicitly by <see cref="CloseAsync(long, CancellationToken)" />, closes the connection with the <see cref="QuicConnectionOptions.DefaultCloseErrorCode"/>.
    /// And releases all resources associated with the connection.
    /// </summary>
    /// <returns>A task that represents the asynchronous dispose operation.</returns>
    public async ValueTask DisposeAsync()
    {
        if (Interlocked.Exchange(ref _disposed, true))
        {
            return;
        }
 
        if (NetEventSource.Log.IsEnabled())
        {
            NetEventSource.Info(this, $"{this} Disposing.");
        }
 
        // Check if the connection has been shut down and if not, shut it down.
        if (_shutdownTcs.TryGetValueTask(out ValueTask valueTask, this))
        {
            unsafe
            {
                MsQuicApi.Api.ConnectionShutdown(
                    _handle,
                    QUIC_CONNECTION_SHUTDOWN_FLAGS.NONE,
                    (ulong)_defaultCloseErrorCode);
            }
        }
        else if (!valueTask.IsCompletedSuccessfully)
        {
            unsafe
            {
                MsQuicApi.Api.ConnectionShutdown(
                    _handle,
                    QUIC_CONNECTION_SHUTDOWN_FLAGS.SILENT,
                    (ulong)_defaultCloseErrorCode);
            }
        }
 
        // Wait for SHUTDOWN_COMPLETE, the last event, so that all resources can be safely released.
        await _shutdownTcs.GetFinalTask(this).ConfigureAwait(false);
        Debug.Assert(_connectedTcs.IsCompleted);
        Debug.Assert(_connectionCloseTcs.Task.IsCompleted);
        _handle.Dispose();
        _shutdownTokenSource.Dispose();
        _connectionCloseTcs.Task.ObserveException();
        _configuration?.Dispose();
 
        // Dispose remote certificate only if it hasn't been accessed via getter, in which case the accessing code becomes the owner of the certificate lifetime.
        if (!_remoteCertificateExposed)
        {
            _remoteCertificate?.Dispose();
        }
 
        // Flush the queue and dispose all remaining streams.
        _acceptQueue.Writer.TryComplete(ExceptionDispatchInfo.SetCurrentStackTrace(new ObjectDisposedException(GetType().FullName)));
        while (_acceptQueue.Reader.TryRead(out QuicStream? stream))
        {
            await stream.DisposeAsync().ConfigureAwait(false);
        }
    }
}