diff --git a/src/libraries/System.Net.Quic/ref/System.Net.Quic.cs b/src/libraries/System.Net.Quic/ref/System.Net.Quic.cs index 1958aed186b99..96a53e2bceb15 100644 --- a/src/libraries/System.Net.Quic/ref/System.Net.Quic.cs +++ b/src/libraries/System.Net.Quic/ref/System.Net.Quic.cs @@ -41,7 +41,10 @@ public abstract partial class QuicConnectionOptions internal QuicConnectionOptions() { } public long DefaultCloseErrorCode { get { throw null; } set { } } public long DefaultStreamErrorCode { get { throw null; } set { } } + public System.TimeSpan HandshakeTimeout { get { throw null; } set { } } public System.TimeSpan IdleTimeout { get { throw null; } set { } } + public System.Net.Quic.QuicReceiveWindowSizes InitialReceiveWindowSizes { get { throw null; } set { } } + public System.TimeSpan KeepAliveInterval { get { throw null; } set { } } public int MaxInboundBidirectionalStreams { get { throw null; } set { } } public int MaxInboundUnidirectionalStreams { get { throw null; } set { } } } @@ -64,8 +67,8 @@ public sealed partial class QuicException : System.IO.IOException { public QuicException(System.Net.Quic.QuicError error, long? applicationErrorCode, string message) { } public long? ApplicationErrorCode { get { throw null; } } - public long? TransportErrorCode { get { throw null; } } public System.Net.Quic.QuicError QuicError { get { throw null; } } + public long? TransportErrorCode { get { throw null; } } } public sealed partial class QuicListener : System.IAsyncDisposable { @@ -85,6 +88,14 @@ public QuicListenerOptions() { } public int ListenBacklog { get { throw null; } set { } } public System.Net.IPEndPoint ListenEndPoint { get { throw null; } set { } } } + public sealed partial class QuicReceiveWindowSizes + { + public QuicReceiveWindowSizes() { } + public int Connection { get { throw null; } set { } } + public int LocallyInitiatedBidirectionalStream { get { throw null; } set { } } + public int RemotelyInitiatedBidirectionalStream { get { throw null; } set { } } + public int UnidirectionalStream { get { throw null; } set { } } + } public sealed partial class QuicServerConnectionOptions : System.Net.Quic.QuicConnectionOptions { public QuicServerConnectionOptions() { } diff --git a/src/libraries/System.Net.Quic/src/ExcludeApiList.PNSE.txt b/src/libraries/System.Net.Quic/src/ExcludeApiList.PNSE.txt index e960c9feb456b..63d6cfde19fea 100644 --- a/src/libraries/System.Net.Quic/src/ExcludeApiList.PNSE.txt +++ b/src/libraries/System.Net.Quic/src/ExcludeApiList.PNSE.txt @@ -2,5 +2,6 @@ P:System.Net.Quic.QuicConnection.IsSupported P:System.Net.Quic.QuicListener.IsSupported C:System.Net.Quic.QuicListenerOptions C:System.Net.Quic.QuicConnectionOptions +C:System.Net.Quic.QuicReceiveWindowSizes C:System.Net.Quic.QuicClientConnectionOptions C:System.Net.Quic.QuicServerConnectionOptions \ No newline at end of file diff --git a/src/libraries/System.Net.Quic/src/Resources/Strings.resx b/src/libraries/System.Net.Quic/src/Resources/Strings.resx index cf02872f9118a..ae0380e50496c 100644 --- a/src/libraries/System.Net.Quic/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Quic/src/Resources/Strings.resx @@ -142,7 +142,10 @@ Writing is not allowed on stream. - '{0}'' should be within [0, {1}) range. + '{0}' should be within [0, {1}) range. + + + '{0}' must be a power of 2. '{0}' must be specified to start the listener. diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicApi.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicApi.cs index e07a99b13f7c7..e89119844c744 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicApi.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicApi.cs @@ -154,7 +154,7 @@ static MsQuicApi() if (version < s_minMsQuicVersion) { - NotSupportedReason = $"Incompatible MsQuic library version '{version}', expecting higher than '{s_minMsQuicVersion}'."; + NotSupportedReason = $"Incompatible MsQuic library version '{version}', expecting higher than '{s_minMsQuicVersion}'."; if (NetEventSource.Log.IsEnabled()) { NetEventSource.Info(null, NotSupportedReason); @@ -178,7 +178,7 @@ static MsQuicApi() // Implies windows platform, check TLS1.3 availability if (!IsWindowsVersionSupported()) { - NotSupportedReason = $"Current Windows version ({Environment.OSVersion}) is not supported by QUIC. Minimal supported version is {s_minWindowsVersion}."; + NotSupportedReason = $"Current Windows version ({Environment.OSVersion}) is not supported by QUIC. Minimal supported version is {s_minWindowsVersion}."; if (NetEventSource.Log.IsEnabled()) { NetEventSource.Info(null, NotSupportedReason); diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicConfiguration.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicConfiguration.cs index 1c3b4872df163..3837e8984cfa8 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicConfiguration.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicConfiguration.cs @@ -117,14 +117,47 @@ private static unsafe MsQuicSafeHandle Create(QuicConnectionOptions options, QUI #pragma warning restore SYSLIB0040 QUIC_SETTINGS settings = default(QUIC_SETTINGS); + settings.IsSet.PeerUnidiStreamCount = 1; settings.PeerUnidiStreamCount = (ushort)options.MaxInboundUnidirectionalStreams; + settings.IsSet.PeerBidiStreamCount = 1; settings.PeerBidiStreamCount = (ushort)options.MaxInboundBidirectionalStreams; + if (options.IdleTimeout != TimeSpan.Zero) { settings.IsSet.IdleTimeoutMs = 1; - settings.IdleTimeoutMs = options.IdleTimeout != Timeout.InfiniteTimeSpan ? (ulong)options.IdleTimeout.TotalMilliseconds : 0; + settings.IdleTimeoutMs = options.IdleTimeout != Timeout.InfiniteTimeSpan + ? (ulong)options.IdleTimeout.TotalMilliseconds + : 0; // 0 disables the timeout + } + + if (options.KeepAliveInterval != TimeSpan.Zero) + { + settings.IsSet.KeepAliveIntervalMs = 1; + settings.KeepAliveIntervalMs = options.KeepAliveInterval != Timeout.InfiniteTimeSpan + ? (uint)options.KeepAliveInterval.TotalMilliseconds + : 0; // 0 disables the keepalive + } + + settings.IsSet.ConnFlowControlWindow = 1; + settings.ConnFlowControlWindow = (uint)(options._initialRecieveWindowSizes?.Connection ?? QuicDefaults.DefaultConnectionMaxData); + + settings.IsSet.StreamRecvWindowBidiLocalDefault = 1; + settings.StreamRecvWindowBidiLocalDefault = (uint)(options._initialRecieveWindowSizes?.LocallyInitiatedBidirectionalStream ?? QuicDefaults.DefaultStreamMaxData); + + settings.IsSet.StreamRecvWindowBidiRemoteDefault = 1; + settings.StreamRecvWindowBidiRemoteDefault = (uint)(options._initialRecieveWindowSizes?.RemotelyInitiatedBidirectionalStream ?? QuicDefaults.DefaultStreamMaxData); + + settings.IsSet.StreamRecvWindowUnidiDefault = 1; + settings.StreamRecvWindowUnidiDefault = (uint)(options._initialRecieveWindowSizes?.UnidirectionalStream ?? QuicDefaults.DefaultStreamMaxData); + + if (options.HandshakeTimeout != TimeSpan.Zero) + { + settings.IsSet.HandshakeIdleTimeoutMs = 1; + settings.HandshakeIdleTimeoutMs = options.HandshakeTimeout != Timeout.InfiniteTimeSpan + ? (ulong)options.HandshakeTimeout.TotalMilliseconds + : 0; // 0 disables the timeout } QUIC_HANDLE* handle; diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Interop/msquic_generated.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Interop/msquic_generated.cs index 2fb9196ae707a..b8c0b092df1df 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Interop/msquic_generated.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Interop/msquic_generated.cs @@ -151,7 +151,7 @@ internal enum QUIC_STREAM_OPEN_FLAGS NONE = 0x0000, UNIDIRECTIONAL = 0x0001, ZERO_RTT = 0x0002, - DELAY_FC_UPDATES = 0x0004, + DELAY_ID_FC_UPDATES = 0x0004, } [System.Flags] @@ -211,6 +211,7 @@ internal enum QUIC_EXECUTION_CONFIG_FLAGS { NONE = 0x0000, QTIP = 0x0001, + RIO = 0x0002, } internal unsafe partial struct QUIC_EXECUTION_CONFIG @@ -764,17 +765,31 @@ internal uint EcnCapable } } - [NativeTypeName("uint32_t : 26")] + [NativeTypeName("uint32_t : 1")] + internal uint EncryptionOffloaded + { + get + { + return (_bitfield >> 6) & 0x1u; + } + + set + { + _bitfield = (_bitfield & ~(0x1u << 6)) | ((value & 0x1u) << 6); + } + } + + [NativeTypeName("uint32_t : 25")] internal uint RESERVED { get { - return (_bitfield >> 6) & 0x3FFFFFFu; + return (_bitfield >> 7) & 0x1FFFFFFu; } set { - _bitfield = (_bitfield & ~(0x3FFFFFFu << 6)) | ((value & 0x3FFFFFFu) << 6); + _bitfield = (_bitfield & ~(0x1FFFFFFu << 7)) | ((value & 0x1FFFFFFu) << 7); } } @@ -1231,6 +1246,15 @@ internal byte EcnEnabled [NativeTypeName("QUIC_SETTINGS::(anonymous union)")] internal _Anonymous2_e__Union Anonymous2; + [NativeTypeName("uint32_t")] + internal uint StreamRecvWindowBidiLocalDefault; + + [NativeTypeName("uint32_t")] + internal uint StreamRecvWindowBidiRemoteDefault; + + [NativeTypeName("uint32_t")] + internal uint StreamRecvWindowUnidiDefault; + internal ref ulong IsSetFlags { get @@ -1268,6 +1292,45 @@ internal ulong HyStartEnabled } } + internal ulong EncryptionOffloadAllowed + { + get + { + return Anonymous2.Anonymous.EncryptionOffloadAllowed; + } + + set + { + Anonymous2.Anonymous.EncryptionOffloadAllowed = value; + } + } + + internal ulong ReliableResetEnabled + { + get + { + return Anonymous2.Anonymous.ReliableResetEnabled; + } + + set + { + Anonymous2.Anonymous.ReliableResetEnabled = value; + } + } + + internal ulong OneWayDelayEnabled + { + get + { + return Anonymous2.Anonymous.OneWayDelayEnabled; + } + + set + { + Anonymous2.Anonymous.OneWayDelayEnabled = value; + } + } + internal ulong ReservedFlags { get @@ -1786,17 +1849,101 @@ internal ulong HyStartEnabled } } - [NativeTypeName("uint64_t : 29")] + [NativeTypeName("uint64_t : 1")] + internal ulong StreamRecvWindowBidiLocalDefault + { + get + { + return (_bitfield >> 35) & 0x1UL; + } + + set + { + _bitfield = (_bitfield & ~(0x1UL << 35)) | ((value & 0x1UL) << 35); + } + } + + [NativeTypeName("uint64_t : 1")] + internal ulong StreamRecvWindowBidiRemoteDefault + { + get + { + return (_bitfield >> 36) & 0x1UL; + } + + set + { + _bitfield = (_bitfield & ~(0x1UL << 36)) | ((value & 0x1UL) << 36); + } + } + + [NativeTypeName("uint64_t : 1")] + internal ulong StreamRecvWindowUnidiDefault + { + get + { + return (_bitfield >> 37) & 0x1UL; + } + + set + { + _bitfield = (_bitfield & ~(0x1UL << 37)) | ((value & 0x1UL) << 37); + } + } + + [NativeTypeName("uint64_t : 1")] + internal ulong EncryptionOffloadAllowed + { + get + { + return (_bitfield >> 38) & 0x1UL; + } + + set + { + _bitfield = (_bitfield & ~(0x1UL << 38)) | ((value & 0x1UL) << 38); + } + } + + [NativeTypeName("uint64_t : 1")] + internal ulong ReliableResetEnabled + { + get + { + return (_bitfield >> 39) & 0x1UL; + } + + set + { + _bitfield = (_bitfield & ~(0x1UL << 39)) | ((value & 0x1UL) << 39); + } + } + + [NativeTypeName("uint64_t : 1")] + internal ulong OneWayDelayEnabled + { + get + { + return (_bitfield >> 40) & 0x1UL; + } + + set + { + _bitfield = (_bitfield & ~(0x1UL << 40)) | ((value & 0x1UL) << 40); + } + } + + [NativeTypeName("uint64_t : 23")] internal ulong RESERVED { get { - return (_bitfield >> 35) & 0x1FFFFFFFUL; + return (_bitfield >> 41) & 0x7FFFFFUL; } set { - _bitfield = (_bitfield & ~(0x1FFFFFFFUL << 35)) | ((value & 0x1FFFFFFFUL) << 35); + _bitfield = (_bitfield & ~(0x7FFFFFUL << 41)) | ((value & 0x7FFFFFUL) << 41); } } } @@ -1831,17 +1978,59 @@ internal ulong HyStartEnabled } } - [NativeTypeName("uint64_t : 63")] + [NativeTypeName("uint64_t : 1")] + internal ulong EncryptionOffloadAllowed + { + get + { + return (_bitfield >> 1) & 0x1UL; + } + + set + { + _bitfield = (_bitfield & ~(0x1UL << 1)) | ((value & 0x1UL) << 1); + } + } + + [NativeTypeName("uint64_t : 1")] + internal ulong ReliableResetEnabled + { + get + { + return (_bitfield >> 2) & 0x1UL; + } + + set + { + _bitfield = (_bitfield & ~(0x1UL << 2)) | ((value & 0x1UL) << 2); + } + } + + [NativeTypeName("uint64_t : 1")] + internal ulong OneWayDelayEnabled + { + get + { + return (_bitfield >> 3) & 0x1UL; + } + + set + { + _bitfield = (_bitfield & ~(0x1UL << 3)) | ((value & 0x1UL) << 3); + } + } + + [NativeTypeName("uint64_t : 60")] internal ulong ReservedFlags { get { - return (_bitfield >> 1) & 0x7FFFFFFFUL; + return (_bitfield >> 4) & 0xFFFFFFFUL; } set { - _bitfield = (_bitfield & ~(0x7FFFFFFFUL << 1)) | ((value & 0x7FFFFFFFUL) << 1); + _bitfield = (_bitfield & ~(0xFFFFFFFUL << 4)) | ((value & 0xFFFFFFFUL) << 4); } } } @@ -2123,6 +2312,8 @@ internal enum QUIC_CONNECTION_EVENT_TYPE RESUMED = 13, RESUMPTION_TICKET_RECEIVED = 14, PEER_CERTIFICATE_RECEIVED = 15, + RELIABLE_RESET_NEGOTIATED = 16, + ONE_WAY_DELAY_NEGOTIATED = 17, } internal partial struct QUIC_CONNECTION_EVENT @@ -2260,6 +2451,22 @@ internal ref _Anonymous_e__Union._PEER_CERTIFICATE_RECEIVED_e__Struct PEER_CERTI } } + internal ref _Anonymous_e__Union._RELIABLE_RESET_NEGOTIATED_e__Struct RELIABLE_RESET_NEGOTIATED + { + get + { + return ref MemoryMarshal.GetReference(MemoryMarshal.CreateSpan(ref Anonymous.RELIABLE_RESET_NEGOTIATED, 1)); + } + } + + internal ref _Anonymous_e__Union._ONE_WAY_DELAY_NEGOTIATED_e__Struct ONE_WAY_DELAY_NEGOTIATED + { + get + { + return ref MemoryMarshal.GetReference(MemoryMarshal.CreateSpan(ref Anonymous.ONE_WAY_DELAY_NEGOTIATED, 1)); + } + } + [StructLayout(LayoutKind.Explicit)] internal partial struct _Anonymous_e__Union { @@ -2327,6 +2534,14 @@ internal partial struct _Anonymous_e__Union [NativeTypeName("struct (anonymous struct)")] internal _PEER_CERTIFICATE_RECEIVED_e__Struct PEER_CERTIFICATE_RECEIVED; + [FieldOffset(0)] + [NativeTypeName("struct (anonymous struct)")] + internal _RELIABLE_RESET_NEGOTIATED_e__Struct RELIABLE_RESET_NEGOTIATED; + + [FieldOffset(0)] + [NativeTypeName("struct (anonymous struct)")] + internal _ONE_WAY_DELAY_NEGOTIATED_e__Struct ONE_WAY_DELAY_NEGOTIATED; + internal unsafe partial struct _CONNECTED_e__Struct { [NativeTypeName("BOOLEAN")] @@ -2440,6 +2655,9 @@ internal partial struct _IDEAL_PROCESSOR_CHANGED_e__Struct { [NativeTypeName("uint16_t")] internal ushort IdealProcessor; + + [NativeTypeName("uint16_t")] + internal ushort PartitionIndex; } internal partial struct _DATAGRAM_STATE_CHANGED_e__Struct @@ -2498,6 +2716,21 @@ internal unsafe partial struct _PEER_CERTIFICATE_RECEIVED_e__Struct [NativeTypeName("QUIC_CERTIFICATE_CHAIN *")] internal void* Chain; } + + internal partial struct _RELIABLE_RESET_NEGOTIATED_e__Struct + { + [NativeTypeName("BOOLEAN")] + internal byte IsNegotiated; + } + + internal partial struct _ONE_WAY_DELAY_NEGOTIATED_e__Struct + { + [NativeTypeName("BOOLEAN")] + internal byte SendNegotiated; + + [NativeTypeName("BOOLEAN")] + internal byte ReceiveNegotiated; + } } } @@ -2895,6 +3128,9 @@ internal static unsafe partial class MsQuic [NativeTypeName("#define QUIC_MAX_RESUMPTION_APP_DATA_LENGTH 1000")] internal const uint QUIC_MAX_RESUMPTION_APP_DATA_LENGTH = 1000; + [NativeTypeName("#define QUIC_STATELESS_RESET_KEY_LENGTH 32")] + internal const uint QUIC_STATELESS_RESET_KEY_LENGTH = 32; + [NativeTypeName("#define QUIC_EXECUTION_CONFIG_MIN_SIZE (uint32_t)FIELD_OFFSET(QUIC_EXECUTION_CONFIG, ProcessorList)")] internal static readonly uint QUIC_EXECUTION_CONFIG_MIN_SIZE = unchecked((uint)((int)(Marshal.OffsetOf("ProcessorList")))); @@ -2961,6 +3197,9 @@ internal static unsafe partial class MsQuic [NativeTypeName("#define QUIC_PARAM_GLOBAL_TLS_PROVIDER 0x0100000A")] internal const uint QUIC_PARAM_GLOBAL_TLS_PROVIDER = 0x0100000A; + [NativeTypeName("#define QUIC_PARAM_GLOBAL_STATELESS_RESET_KEY 0x0100000B")] + internal const uint QUIC_PARAM_GLOBAL_STATELESS_RESET_KEY = 0x0100000B; + [NativeTypeName("#define QUIC_PARAM_CONFIGURATION_SETTINGS 0x03000000")] internal const uint QUIC_PARAM_CONFIGURATION_SETTINGS = 0x03000000; @@ -3054,6 +3293,9 @@ internal static unsafe partial class MsQuic [NativeTypeName("#define QUIC_PARAM_CONN_STATISTICS_V2_PLAT 0x05000017")] internal const uint QUIC_PARAM_CONN_STATISTICS_V2_PLAT = 0x05000017; + [NativeTypeName("#define QUIC_PARAM_CONN_ORIG_DEST_CID 0x05000018")] + internal const uint QUIC_PARAM_CONN_ORIG_DEST_CID = 0x05000018; + [NativeTypeName("#define QUIC_PARAM_TLS_HANDSHAKE_INFO 0x06000000")] internal const uint QUIC_PARAM_TLS_HANDSHAKE_INFO = 0x06000000; @@ -3084,6 +3326,9 @@ internal static unsafe partial class MsQuic [NativeTypeName("#define QUIC_PARAM_STREAM_STATISTICS 0X08000004")] internal const uint QUIC_PARAM_STREAM_STATISTICS = 0X08000004; + [NativeTypeName("#define QUIC_PARAM_STREAM_RELIABLE_OFFSET 0x08000005")] + internal const uint QUIC_PARAM_STREAM_RELIABLE_OFFSET = 0x08000005; + [NativeTypeName("#define QUIC_API_VERSION_2 2")] internal const uint QUIC_API_VERSION_2 = 2; } diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs index 5f3d4f16deb4b..be4a8f8f639b0 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs @@ -66,9 +66,25 @@ public static ValueTask ConnectAsync(QuicClientConnectionOptions static async ValueTask 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, cancellationToken).ConfigureAwait(false); + await connection.FinishConnectAsync(options, linkedCts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + // handshake timeout elapsed, tear down the connection. + // Note that since handshake is not done yet, application error code is not sent. + await connection.DisposeAsync().ConfigureAwait(false); + + throw new QuicException(QuicError.ConnectionTimeout, null, SR.Format(SR.net_quic_handshake_timeout, options.HandshakeTimeout)); } catch { @@ -93,6 +109,11 @@ static async ValueTask StartConnectAsync(QuicClientConnectionOpt private readonly ValueTaskSource _connectedTcs = new ValueTaskSource(); private readonly ValueTaskSource _shutdownTcs = new ValueTaskSource(); + private readonly CancellationTokenSource _shutdownTokenSource = new CancellationTokenSource(); + + // Token that fires when the connection is closed. + internal CancellationToken ConnectionShutdownToken => _shutdownTokenSource.Token; + private readonly Channel _acceptQueue = Channel.CreateUnbounded(new UnboundedChannelOptions() { SingleWriter = true @@ -496,6 +517,7 @@ private unsafe int HandleEventShutdownComplete() Exception exception = ExceptionDispatchInfo.SetCurrentStackTrace(_disposed == 1 ? new ObjectDisposedException(GetType().FullName) : ThrowHelper.GetOperationAbortedException()); _acceptQueue.Writer.TryComplete(exception); _connectedTcs.TrySetException(exception); + _shutdownTokenSource.Cancel(); _shutdownTcs.TrySetResult(); return QUIC_STATUS_SUCCESS; } @@ -523,7 +545,7 @@ private unsafe int HandleEventPeerStreamStarted(ref PEER_STREAM_STARTED_DATA dat return QUIC_STATUS_SUCCESS; } - data.Flags |= QUIC_STREAM_OPEN_FLAGS.DELAY_FC_UPDATES; + data.Flags |= QUIC_STREAM_OPEN_FLAGS.DELAY_ID_FC_UPDATES; return QUIC_STATUS_SUCCESS; } private unsafe int HandleEventPeerCertificateReceived(ref PEER_CERTIFICATE_RECEIVED_DATA data) @@ -617,6 +639,7 @@ public async ValueTask DisposeAsync() await valueTask.ConfigureAwait(false); Debug.Assert(_connectedTcs.IsCompleted); _handle.Dispose(); + _shutdownTokenSource.Dispose(); _configuration?.Dispose(); diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnectionOptions.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnectionOptions.cs index 67f4ab96d7888..81a20eacad951 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnectionOptions.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnectionOptions.cs @@ -2,10 +2,53 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Security; +using System.Runtime.CompilerServices; using System.Threading; namespace System.Net.Quic; +/// +/// Collection of receive window sizes for as a whole and for individual types. +/// +public sealed class QuicReceiveWindowSizes +{ + /// + /// The initial flow-control window size for the connection. + /// + public int Connection { get; set; } = QuicDefaults.DefaultConnectionMaxData; + + /// + /// The initial flow-control window size for locally initiated bidirectional streams. + /// + public int LocallyInitiatedBidirectionalStream { get; set; } = QuicDefaults.DefaultStreamMaxData; + + /// + /// The initial flow-control window size for remotely initiated bidirectional streams. + /// + public int RemotelyInitiatedBidirectionalStream { get; set; } = QuicDefaults.DefaultStreamMaxData; + + /// + /// The initial flow-control window size for (remotely initiated) unidirectional streams. + /// + public int UnidirectionalStream { get; set; } = QuicDefaults.DefaultStreamMaxData; + + internal void Validate(string argumentName) + { + ValidatePowerOf2(argumentName, Connection); + ValidatePowerOf2(argumentName, LocallyInitiatedBidirectionalStream); + ValidatePowerOf2(argumentName, RemotelyInitiatedBidirectionalStream); + ValidatePowerOf2(argumentName, UnidirectionalStream); + + static void ValidatePowerOf2(string argumentName, int value, [CallerArgumentExpression(nameof(value))] string? propertyName = null) + { + if (value <= 0 || ((value - 1) & value) != 0) + { + throw new ArgumentOutOfRangeException(argumentName, value, SR.Format(SR.net_quic_power_of_2, $"{nameof(QuicConnectionOptions.InitialReceiveWindowSizes)}.{propertyName}")); + } + } + } +} + /// /// Shared options for both client (outbound) and server (inbound) . /// @@ -54,31 +97,62 @@ internal QuicConnectionOptions() // We can safely use this to distinguish if user provided value during validation. public long DefaultCloseErrorCode { get; set; } = -1; + internal QuicReceiveWindowSizes? _initialRecieveWindowSizes; + + /// + /// The initial receive window sizes for the connection and individual stream types. + /// + public QuicReceiveWindowSizes InitialReceiveWindowSizes + { + get => _initialRecieveWindowSizes ??= new QuicReceiveWindowSizes(); + set => _initialRecieveWindowSizes = value; + } + + /// + /// The interval at which keep alive packets are sent on the connection. + /// Value means underlying implementation default timeout. + /// Default means never sending keep alive packets. + /// + public TimeSpan KeepAliveInterval { get; set; } = Timeout.InfiniteTimeSpan; + + /// + /// The upper bound on time when the handshake must complete. If the handshake does not + /// complete in this time, the connection is aborted. + /// Value means underlying implementation default timeout. + /// Default timeout is 10 seconds. + /// + public TimeSpan HandshakeTimeout { get; set; } = QuicDefaults.HandshakeTimeout; + /// /// Validates the options and potentially sets platform specific defaults. /// /// Name of the from the caller. internal virtual void Validate(string argumentName) { - if (MaxInboundBidirectionalStreams < 0 || MaxInboundBidirectionalStreams > ushort.MaxValue) - { - throw new ArgumentOutOfRangeException(SR.Format(SR.net_quic_in_range, nameof(QuicConnectionOptions.MaxInboundBidirectionalStreams), ushort.MaxValue), argumentName); - } - if (MaxInboundUnidirectionalStreams < 0 || MaxInboundUnidirectionalStreams > ushort.MaxValue) - { - throw new ArgumentOutOfRangeException(SR.Format(SR.net_quic_in_range, nameof(QuicConnectionOptions.MaxInboundUnidirectionalStreams), ushort.MaxValue), argumentName); - } - if (IdleTimeout < TimeSpan.Zero && IdleTimeout != Timeout.InfiniteTimeSpan) - { - throw new ArgumentOutOfRangeException(nameof(QuicConnectionOptions.IdleTimeout), SR.net_quic_timeout_use_gt_zero); - } - if (DefaultStreamErrorCode < 0 || DefaultStreamErrorCode > QuicDefaults.MaxErrorCodeValue) + ValidateInRange(argumentName, MaxInboundBidirectionalStreams, ushort.MaxValue); + ValidateInRange(argumentName, MaxInboundUnidirectionalStreams, ushort.MaxValue); + ValidateTimespan(argumentName, IdleTimeout); + ValidateTimespan(argumentName, KeepAliveInterval); + ValidateInRange(argumentName, DefaultCloseErrorCode, QuicDefaults.MaxErrorCodeValue); + ValidateInRange(argumentName, DefaultStreamErrorCode, QuicDefaults.MaxErrorCodeValue); + ValidateTimespan(argumentName, HandshakeTimeout); + + _initialRecieveWindowSizes?.Validate(argumentName); + + static void ValidateInRange(string argumentName, long value, long max, [CallerArgumentExpression(nameof(value))] string? propertyName = null) { - throw new ArgumentOutOfRangeException(SR.Format(SR.net_quic_in_range, nameof(QuicConnectionOptions.DefaultStreamErrorCode), QuicDefaults.MaxErrorCodeValue), argumentName); + if (value < 0 || value > max) + { + throw new ArgumentOutOfRangeException(argumentName, value, SR.Format(SR.net_quic_in_range, propertyName, max)); + } } - if (DefaultCloseErrorCode < 0 || DefaultCloseErrorCode > QuicDefaults.MaxErrorCodeValue) + + static void ValidateTimespan(string argumentName, TimeSpan value, [CallerArgumentExpression(nameof(value))] string? propertyName = null) { - throw new ArgumentOutOfRangeException(SR.Format(SR.net_quic_in_range, nameof(QuicConnectionOptions.DefaultCloseErrorCode), QuicDefaults.MaxErrorCodeValue), argumentName); + if (value < TimeSpan.Zero && value != Timeout.InfiniteTimeSpan) + { + throw new ArgumentOutOfRangeException(argumentName, value, SR.Format(SR.net_quic_timeout_use_gt_zero, propertyName)); + } } } } @@ -123,13 +197,15 @@ internal override void Validate(string argumentName) base.Validate(argumentName); // The content of ClientAuthenticationOptions gets validate in MsQuicConfiguration.Create. - if (ClientAuthenticationOptions is null) - { - throw new ArgumentNullException(SR.Format(SR.net_quic_not_null_open_connection, nameof(QuicClientConnectionOptions.ClientAuthenticationOptions)), argumentName); - } - if (RemoteEndPoint is null) + ValidateNotNull(argumentName, ClientAuthenticationOptions); + ValidateNotNull(argumentName, RemoteEndPoint); + + static void ValidateNotNull(string argumentName, object value, [CallerArgumentExpression(nameof(value))] string? propertyName = null) { - throw new ArgumentNullException(SR.Format(SR.net_quic_not_null_open_connection, nameof(QuicClientConnectionOptions.RemoteEndPoint)), argumentName); + if (value is null) + { + throw new ArgumentNullException(argumentName, SR.Format(SR.net_quic_not_null_open_connection, propertyName)); + } } } } @@ -163,9 +239,14 @@ internal override void Validate(string argumentName) base.Validate(argumentName); // The content of ServerAuthenticationOptions gets validate in MsQuicConfiguration.Create. - if (ServerAuthenticationOptions is null) + ValidateNotNull(argumentName, ServerAuthenticationOptions); + + static void ValidateNotNull(string argumentName, object value, [CallerArgumentExpression(nameof(value))] string? propertyName = null) { - throw new ArgumentNullException(SR.Format(SR.net_quic_not_null_accept_connection, nameof(QuicServerConnectionOptions.ServerAuthenticationOptions)), argumentName); + if (value is null) + { + throw new ArgumentNullException(argumentName, SR.Format(SR.net_quic_not_null_accept_connection, propertyName)); + } } } } diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicDefaults.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicDefaults.cs index 4715effc5dbaf..e31bc1d21c20d 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicDefaults.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicDefaults.cs @@ -34,7 +34,17 @@ internal static partial class QuicDefaults public const long MaxErrorCodeValue = (1L << 62) - 1; /// - /// Our own imposed timeout in the handshake process, since in certain cases MsQuic will not impose theirs, see . + /// Default handshake timeout. /// public static readonly TimeSpan HandshakeTimeout = TimeSpan.FromSeconds(10); + + /// + /// Default initial_max_data value. + /// + public static int DefaultConnectionMaxData = 16 * 1024 * 1024; + + /// + /// Default initial_max_stream_data_* value. + /// + public static int DefaultStreamMaxData = 64 * 1024; } diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs index 8ecbfb9901eb2..e39d718718d11 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs @@ -1,6 +1,7 @@ // 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.Runtime.CompilerServices; using System.Runtime.ExceptionServices; @@ -210,15 +211,28 @@ private async void StartConnectionHandshake(QuicConnection connection, SslClient { bool wrapException = false; CancellationToken cancellationToken = default; + + // In certain cases MsQuic will not impose the handshake idle timeout on their side, see + // https://github.com/microsoft/msquic/discussions/2705. + // This will be assigned to before the linked CTS is cancelled + TimeSpan handshakeTimeout = QuicDefaults.HandshakeTimeout; try { - using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_disposeCts.Token); - linkedCts.CancelAfter(QuicDefaults.HandshakeTimeout); + using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_disposeCts.Token, connection.ConnectionShutdownToken); cancellationToken = linkedCts.Token; + // initial timeout for retrieving connection options + linkedCts.CancelAfter(handshakeTimeout); + wrapException = true; QuicServerConnectionOptions options = await _connectionOptionsCallback(connection, clientHello, cancellationToken).ConfigureAwait(false); wrapException = false; - options.Validate(nameof(options)); // Validate and fill in defaults for the options. + + options.Validate(nameof(options)); + + // update handshake timetout based on the returned value + handshakeTimeout = options.HandshakeTimeout; + linkedCts.CancelAfter(handshakeTimeout); + await connection.FinishHandshakeAsync(options, clientHello.ServerName, cancellationToken).ConfigureAwait(false); if (!_acceptQueue.Writer.TryWrite(connection)) { @@ -226,6 +240,28 @@ private async void StartConnectionHandshake(QuicConnection connection, SslClient await connection.DisposeAsync().ConfigureAwait(false); } } + catch (OperationCanceledException) when (connection.ConnectionShutdownToken.IsCancellationRequested) + { + // Connection closed by peer + if (NetEventSource.Log.IsEnabled()) + { + NetEventSource.Info(connection, $"{connection} Connection closed by remote peer"); + } + + // retrieve the exception which failed the handshake, the parameters are not going to be + // validated because the inner _connectedTcs is already transitioned to faulted state + ValueTask task = connection.FinishHandshakeAsync(null!, null!, default); + Debug.Assert(task.IsFaulted); + + // unwrap AggregateException and propagate it to the accept queue + Exception ex = task.AsTask().Exception!.InnerException!; + + await connection.DisposeAsync().ConfigureAwait(false); + if (!_acceptQueue.Writer.TryWrite(ex)) + { + // Channel has been closed, connection is already disposed, do nothing. + } + } catch (OperationCanceledException) when (_disposeCts.IsCancellationRequested) { // Handshake stopped by QuicListener.DisposeAsync: @@ -241,7 +277,7 @@ private async void StartConnectionHandshake(QuicConnection connection, SslClient } catch (OperationCanceledException oce) when (cancellationToken.IsCancellationRequested) { - // Handshake cancelled by QuicDefaults.HandshakeTimeout, probably stalled: + // Handshake cancelled by options.HandshakeTimeout, probably stalled: // 1. Connection must be killed so dispose it and by that shut it down --> application error code doesn't matter here as this is a transport error. // 2. Connection won't be handed out since it's useless --> propagate appropriate exception, listener will pass it to the caller. @@ -250,7 +286,7 @@ private async void StartConnectionHandshake(QuicConnection connection, SslClient NetEventSource.Error(connection, $"{connection} Connection handshake timed out: {oce}"); } - Exception ex = ExceptionDispatchInfo.SetCurrentStackTrace(new QuicException(QuicError.ConnectionTimeout, null, SR.Format(SR.net_quic_handshake_timeout, QuicDefaults.HandshakeTimeout), oce)); + Exception ex = ExceptionDispatchInfo.SetCurrentStackTrace(new QuicException(QuicError.ConnectionTimeout, null, SR.Format(SR.net_quic_handshake_timeout, handshakeTimeout), oce)); await connection.DisposeAsync().ConfigureAwait(false); if (!_acceptQueue.Writer.TryWrite(ex)) { diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs index b63ee10c5834f..56767f72a82c9 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs @@ -135,10 +135,12 @@ public async Task AcceptConnectionAsync_ThrowingCallbackOde_KeepRunning() } [Theory] - [InlineData(true)] - [InlineData(false)] + [InlineData(true, true)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(false, false)] [OuterLoop("Exercises several seconds long timeout.")] - public async Task AcceptConnectionAsync_SlowOptionsCallback_TimesOut(bool useCancellationToken) + public async Task AcceptConnectionAsync_SlowOptionsCallback_TimesOut(bool useCancellationToken, bool clientShorterTimeout) { QuicListenerOptions listenerOptions = CreateQuicListenerOptions(); // Stall the options callback to force the timeout. @@ -156,13 +158,60 @@ public async Task AcceptConnectionAsync_SlowOptionsCallback_TimesOut(bool useCan }; await using QuicListener listener = await CreateQuicListener(listenerOptions); - ValueTask connectTask = CreateQuicConnection(listener.LocalEndPoint); - Exception exception = await AssertThrowsQuicExceptionAsync(QuicError.ConnectionTimeout, async () => await listener.AcceptConnectionAsync()); - Assert.Equal(SR.Format(SR.net_quic_handshake_timeout, QuicDefaults.HandshakeTimeout), exception.Message); + QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(listener.LocalEndPoint); + clientOptions.HandshakeTimeout = clientShorterTimeout ? TimeSpan.FromSeconds(2) : TimeSpan.FromSeconds(20); - // Connect attempt should be stopped with "UserCanceled". - var connectException = await Assert.ThrowsAsync(async () => await connectTask); + ValueTask connectTask = CreateQuicConnection(clientOptions); + + if (clientShorterTimeout) + { + // Client gave up earlier than server and aborts the handshake, server should get UserCanceled + // TLS alert + var connectException = await Assert.ThrowsAsync(async () => await listener.AcceptConnectionAsync()); + Assert.Contains(TlsAlertMessage.UserCanceled.ToString(), connectException.Message); + + var exception = await AssertThrowsQuicExceptionAsync(QuicError.ConnectionTimeout, async () => await connectTask); + Assert.Equal(SR.Format(SR.net_quic_handshake_timeout, clientOptions.HandshakeTimeout), exception.Message); + } + else + { + // handshake timed out on server side, expect ConnectionTimeout + Exception exception = await AssertThrowsQuicExceptionAsync(QuicError.ConnectionTimeout, async () => await listener.AcceptConnectionAsync()); + Assert.Equal(SR.Format(SR.net_quic_handshake_timeout, QuicDefaults.HandshakeTimeout), exception.Message); + + // Server aborts the handshake while client is still waiting, so UserCanceled alert is expected + var connectException = await Assert.ThrowsAsync(async () => await connectTask); + Assert.Contains(TlsAlertMessage.UserCanceled.ToString(), connectException.Message); + } + } + + [Fact] + [OuterLoop("Exercises several seconds long timeout.")] + public async Task AcceptConnectionAsync_ClientCancels_FiresOptionCallbackCancellationToken() + { + QuicListenerOptions listenerOptions = CreateQuicListenerOptions(); + listenerOptions.ConnectionOptionsCallback = async (connection, hello, cancellationToken) => + { + // default timeout for callback is 10s, wait for 5s for cancellation from client + // terminating the connection mid-handshake + var oce = await Assert.ThrowsAnyAsync(() => Task.Delay(TimeSpan.FromSeconds(5), cancellationToken)); + Assert.True(cancellationToken.IsCancellationRequested); + Assert.Equal(cancellationToken, oce.CancellationToken); + ExceptionDispatchInfo.Throw(oce); + return null; // unreached; + }; + await using QuicListener listener = await CreateQuicListener(listenerOptions); + + QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(listener.LocalEndPoint); + clientOptions.HandshakeTimeout = TimeSpan.FromSeconds(0.1); + + ValueTask connectTask = CreateQuicConnection(clientOptions); + + var connectException = await Assert.ThrowsAsync(async () => await listener.AcceptConnectionAsync()); Assert.Contains(TlsAlertMessage.UserCanceled.ToString(), connectException.Message); + + var exception = await AssertThrowsQuicExceptionAsync(QuicError.ConnectionTimeout, async () => await connectTask); + Assert.Equal(SR.Format(SR.net_quic_handshake_timeout, clientOptions.HandshakeTimeout), exception.Message); } [Fact] @@ -352,7 +401,7 @@ public async Task ListenOnAlreadyUsedPort_Throws_AddressInUse() // Try to create a listener on the same port. SocketException ex = await Assert.ThrowsAsync(() => CreateQuicListener((IPEndPoint)s.LocalEndPoint).AsTask()); - Assert.Equal(SocketError.AddressAlreadyInUse, ((SocketException)ex).SocketErrorCode ); + Assert.Equal(SocketError.AddressAlreadyInUse, ((SocketException)ex).SocketErrorCode); } [Fact]