From 92bcf06c44759b0d09360d8e45a2bb5ccf5193fe Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Tue, 31 Oct 2023 14:26:02 +0100 Subject: [PATCH 1/8] [API Proposal]: Additional QuicConnectionOptions --- .../System.Net.Quic/ref/System.Net.Quic.cs | 8 +- .../src/System/Net/Quic/Internal/MsQuicApi.cs | 5 +- .../Net/Quic/Internal/MsQuicConfiguration.cs | 34 +++ .../Net/Quic/Interop/msquic_generated.cs | 265 +++++++++++++++++- .../src/System/Net/Quic/QuicConnection.cs | 2 +- .../System/Net/Quic/QuicConnectionOptions.cs | 36 +++ 6 files changed, 336 insertions(+), 14 deletions(-) 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 1958aed186b996..6bffba5d40b92f 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,13 @@ 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 int InitialConnectionWindowSize { get { throw null; } set { } } + public int InitialLocallyInitiatedBidirectionalStreamReceiveWindowSize { get { throw null; } set { } } + public int InitialRemotelyInitiatedBidirectionalStreamReceiveWindowSize { get { throw null; } set { } } + public int InitialUnidirectionalStreamReceiveWindowSize { 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 +70,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 { 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 e07a99b13f7c70..f4dfb68376acec 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 @@ -18,6 +18,7 @@ internal sealed unsafe partial class MsQuicApi { private static readonly Version s_minWindowsVersion = new Version(10, 0, 20145, 1000); + // TODO: update after https://github.com/microsoft/msquic/pull/3948 is released private static readonly Version s_minMsQuicVersion = new Version(2, 2, 2); private static readonly delegate* unmanaged[Cdecl] MsQuicOpenVersion; @@ -154,7 +155,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 +179,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 1c3b4872df1636..ff72b422b22c25 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,16 +117,50 @@ 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; } + if (options.KeepAliveInterval != TimeSpan.Zero) + { + settings.IsSet.KeepAliveIntervalMs = 1; + settings.KeepAliveIntervalMs = (uint)options.KeepAliveInterval.TotalMilliseconds; + } + + if (options.InitialConnectionWindowSize > 0) + { + settings.IsSet.ConnFlowControlWindow = 1; + settings.ConnFlowControlWindow = (uint)options.InitialConnectionWindowSize; + } + + // todo: these values need all to be powers of 2 + if (options.InitialLocallyInitiatedBidirectionalStreamReceiveWindowSize > 0) + { + settings.IsSet.StreamRecvWindowBidiLocalDefault = 1; + settings.StreamRecvWindowBidiLocalDefault = (uint)options.InitialLocallyInitiatedBidirectionalStreamReceiveWindowSize; + } + + if (options.InitialRemotelyInitiatedBidirectionalStreamReceiveWindowSize > 0) + { + settings.IsSet.StreamRecvWindowBidiRemoteDefault = 1; + settings.StreamRecvWindowBidiRemoteDefault = (uint)options.InitialRemotelyInitiatedBidirectionalStreamReceiveWindowSize; + } + + if (options.InitialUnidirectionalStreamReceiveWindowSize > 0) + { + settings.IsSet.StreamRecvWindowUnidiDefault = 1; + settings.StreamRecvWindowUnidiDefault = (uint)options.InitialUnidirectionalStreamReceiveWindowSize; + } + QUIC_HANDLE* handle; using MsQuicBuffers msquicBuffers = new MsQuicBuffers(); 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 2fb9196ae707ad..b8c0b092df1df7 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 5f3d4f16deb4b1..24fb799b51f976 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 @@ -523,7 +523,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) 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 67f4ab96d78884..6b723adcd51841 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 @@ -54,6 +54,42 @@ internal QuicConnectionOptions() // We can safely use this to distinguish if user provided value during validation. public long DefaultCloseErrorCode { get; set; } = -1; + /// + /// The interval at which keep alive packets are sent on the connection. + /// Default means underlying implementation default interval. + /// + public TimeSpan KeepAliveInterval { get; set; } = TimeSpan.Zero; + + /// + /// The initial flow-control window size for the connection. + /// Default 0 to leave the window size to the implementation. + /// + public int InitialConnectionWindowSize { get; set; } = 0; + + /// + /// The initial flow-control window size for locally initiated bidirectional streams. + /// Default 0 to leave the window size to the implementation. + /// + public int InitialLocallyInitiatedBidirectionalStreamReceiveWindowSize { get; set; } = 0; + + /// + /// The initial flow-control window size for remotely initiated bidirectional streams. + /// Default 0 to leave the window size to the implementation. + /// + public int InitialRemotelyInitiatedBidirectionalStreamReceiveWindowSize { get; set; } = 0; + + /// + /// The initial flow-control window size for (remotely initiated) unidirectional streams. + /// Default 0 to leave the window size to the implementation. + /// + public int InitialUnidirectionalStreamReceiveWindowSize { get; set; } = 0; + + /// + /// The initial flow-control window size for (remotely initiated) unidirectional streams. + /// Default 0 to leave the window size to the implementation. + /// + public TimeSpan HandshakeTimeout { get; set; } = TimeSpan.Zero; + /// /// Validates the options and potentially sets platform specific defaults. /// From d26aab82fe739c5fc29a493211da2e3953ce9944 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Thu, 2 Nov 2023 12:18:05 +0100 Subject: [PATCH 2/8] Comment change --- .../src/System/Net/Quic/QuicConnectionOptions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 6b723adcd51841..ae7a5f94be2639 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 @@ -85,8 +85,8 @@ internal QuicConnectionOptions() public int InitialUnidirectionalStreamReceiveWindowSize { get; set; } = 0; /// - /// The initial flow-control window size for (remotely initiated) unidirectional streams. - /// Default 0 to leave the window size to the implementation. + /// The upper bound on time when the handshake must complete. If the handshake does not + /// complete in this time, the connection is aborted. /// public TimeSpan HandshakeTimeout { get; set; } = TimeSpan.Zero; From 96dccbff8afdd72feff69cf1045773f66285d32d Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Wed, 15 Nov 2023 13:41:20 +0100 Subject: [PATCH 3/8] Reflect latest API review --- .../System.Net.Quic/ref/System.Net.Quic.cs | 13 +- .../src/ExcludeApiList.PNSE.txt | 1 + .../src/Resources/Strings.resx | 5 +- .../Net/Quic/Internal/MsQuicConfiguration.cs | 35 ++--- .../System/Net/Quic/QuicConnectionOptions.cs | 137 ++++++++++++------ 5 files changed, 114 insertions(+), 77 deletions(-) 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 6bffba5d40b92f..96a53e2bceb155 100644 --- a/src/libraries/System.Net.Quic/ref/System.Net.Quic.cs +++ b/src/libraries/System.Net.Quic/ref/System.Net.Quic.cs @@ -43,10 +43,7 @@ internal QuicConnectionOptions() { } public long DefaultStreamErrorCode { get { throw null; } set { } } public System.TimeSpan HandshakeTimeout { get { throw null; } set { } } public System.TimeSpan IdleTimeout { get { throw null; } set { } } - public int InitialConnectionWindowSize { get { throw null; } set { } } - public int InitialLocallyInitiatedBidirectionalStreamReceiveWindowSize { get { throw null; } set { } } - public int InitialRemotelyInitiatedBidirectionalStreamReceiveWindowSize { get { throw null; } set { } } - public int InitialUnidirectionalStreamReceiveWindowSize { 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 { } } @@ -91,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 e960c9feb456bd..63d6cfde19fea1 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 cf02872f9118a5..ae0380e50496c0 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/MsQuicConfiguration.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicConfiguration.cs index ff72b422b22c25..ebc3c1a1e5ead2 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 @@ -124,42 +124,29 @@ private static unsafe MsQuicSafeHandle Create(QuicConnectionOptions options, QUI settings.IsSet.PeerBidiStreamCount = 1; settings.PeerBidiStreamCount = (ushort)options.MaxInboundBidirectionalStreams; - if (options.IdleTimeout != TimeSpan.Zero) + if (options.IdleTimeout != Timeout.InfiniteTimeSpan) { settings.IsSet.IdleTimeoutMs = 1; - settings.IdleTimeoutMs = options.IdleTimeout != Timeout.InfiniteTimeSpan ? (ulong)options.IdleTimeout.TotalMilliseconds : 0; + settings.IdleTimeoutMs = (uint)options.IdleTimeout.TotalMilliseconds; } - if (options.KeepAliveInterval != TimeSpan.Zero) + if (options.KeepAliveInterval != Timeout.InfiniteTimeSpan) { settings.IsSet.KeepAliveIntervalMs = 1; settings.KeepAliveIntervalMs = (uint)options.KeepAliveInterval.TotalMilliseconds; } - if (options.InitialConnectionWindowSize > 0) - { - settings.IsSet.ConnFlowControlWindow = 1; - settings.ConnFlowControlWindow = (uint)options.InitialConnectionWindowSize; - } + settings.IsSet.ConnFlowControlWindow = 1; + settings.ConnFlowControlWindow = (uint)options.InitialReceiveWindowSizes.Connection; - // todo: these values need all to be powers of 2 - if (options.InitialLocallyInitiatedBidirectionalStreamReceiveWindowSize > 0) - { - settings.IsSet.StreamRecvWindowBidiLocalDefault = 1; - settings.StreamRecvWindowBidiLocalDefault = (uint)options.InitialLocallyInitiatedBidirectionalStreamReceiveWindowSize; - } + settings.IsSet.StreamRecvWindowBidiLocalDefault = 1; + settings.StreamRecvWindowBidiLocalDefault = (uint)options.InitialReceiveWindowSizes.LocallyInitiatedBidirectionalStream; - if (options.InitialRemotelyInitiatedBidirectionalStreamReceiveWindowSize > 0) - { - settings.IsSet.StreamRecvWindowBidiRemoteDefault = 1; - settings.StreamRecvWindowBidiRemoteDefault = (uint)options.InitialRemotelyInitiatedBidirectionalStreamReceiveWindowSize; - } + settings.IsSet.StreamRecvWindowBidiRemoteDefault = 1; + settings.StreamRecvWindowBidiRemoteDefault = (uint)options.InitialReceiveWindowSizes.RemotelyInitiatedBidirectionalStream; - if (options.InitialUnidirectionalStreamReceiveWindowSize > 0) - { - settings.IsSet.StreamRecvWindowUnidiDefault = 1; - settings.StreamRecvWindowUnidiDefault = (uint)options.InitialUnidirectionalStreamReceiveWindowSize; - } + settings.IsSet.StreamRecvWindowUnidiDefault = 1; + settings.StreamRecvWindowUnidiDefault = (uint)options.InitialReceiveWindowSizes.UnidirectionalStream; QUIC_HANDLE* handle; 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 ae7a5f94be2639..550d68dbfb65a3 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 individual types. +/// +public sealed class QuicReceiveWindowSizes +{ + /// + /// The initial flow-control window size for the connection. + /// + public int Connection { get; set; } = 16 * 1024 * 1024; + + /// + /// The initial flow-control window size for locally initiated bidirectional streams. + /// + public int LocallyInitiatedBidirectionalStream { get; set; } = 64 * 1024; + + /// + /// The initial flow-control window size for remotely initiated bidirectional streams. + /// + public int RemotelyInitiatedBidirectionalStream { get; set; } = 64 * 1024; + + /// + /// The initial flow-control window size for (remotely initiated) unidirectional streams. + /// + public int UnidirectionalStream { get; set; } = 64 * 1024; + + 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,41 +97,27 @@ internal QuicConnectionOptions() // We can safely use this to distinguish if user provided value during validation. public long DefaultCloseErrorCode { get; set; } = -1; - /// - /// The interval at which keep alive packets are sent on the connection. - /// Default means underlying implementation default interval. - /// - public TimeSpan KeepAliveInterval { get; set; } = TimeSpan.Zero; - - /// - /// The initial flow-control window size for the connection. - /// Default 0 to leave the window size to the implementation. - /// - public int InitialConnectionWindowSize { get; set; } = 0; - - /// - /// The initial flow-control window size for locally initiated bidirectional streams. - /// Default 0 to leave the window size to the implementation. - /// - public int InitialLocallyInitiatedBidirectionalStreamReceiveWindowSize { get; set; } = 0; + private QuicReceiveWindowSizes? _initialRecieveWindowSizes; /// - /// The initial flow-control window size for remotely initiated bidirectional streams. - /// Default 0 to leave the window size to the implementation. + /// The initial receive window sizes for the connection and individual stream types. /// - public int InitialRemotelyInitiatedBidirectionalStreamReceiveWindowSize { get; set; } = 0; + public QuicReceiveWindowSizes InitialReceiveWindowSizes + { + get => _initialRecieveWindowSizes ??= new QuicReceiveWindowSizes(); + set => _initialRecieveWindowSizes = value; + } /// - /// The initial flow-control window size for (remotely initiated) unidirectional streams. - /// Default 0 to leave the window size to the implementation. + /// The interval at which keep alive packets are sent on the connection. /// - public int InitialUnidirectionalStreamReceiveWindowSize { get; set; } = 0; + 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. /// - public TimeSpan HandshakeTimeout { get; set; } = TimeSpan.Zero; + public TimeSpan HandshakeTimeout { get; set; } = TimeSpan.FromSeconds(10); /// /// Validates the options and potentially sets platform specific defaults. @@ -96,25 +125,30 @@ internal QuicConnectionOptions() /// 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)); + } } } } @@ -159,13 +193,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)); + } } } } @@ -199,9 +235,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)); + } } } } From 4411b523b484cffa4a0d932701d8ee264bebe821 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Wed, 15 Nov 2023 18:27:35 +0100 Subject: [PATCH 4/8] Use HandshakeTimeout --- .../Net/Quic/Internal/MsQuicConfiguration.cs | 20 ++++++++++++------- .../System/Net/Quic/QuicConnectionOptions.cs | 2 +- .../src/System/Net/Quic/QuicDefaults.cs | 2 +- .../src/System/Net/Quic/QuicListener.cs | 17 ++++++++++++---- 4 files changed, 28 insertions(+), 13 deletions(-) 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 ebc3c1a1e5ead2..5c4f61cd6791a0 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 @@ -124,17 +124,18 @@ private static unsafe MsQuicSafeHandle Create(QuicConnectionOptions options, QUI settings.IsSet.PeerBidiStreamCount = 1; settings.PeerBidiStreamCount = (ushort)options.MaxInboundBidirectionalStreams; - if (options.IdleTimeout != Timeout.InfiniteTimeSpan) + if (options.IdleTimeout != TimeSpan.Zero) { settings.IsSet.IdleTimeoutMs = 1; - settings.IdleTimeoutMs = (uint)options.IdleTimeout.TotalMilliseconds; + settings.IdleTimeoutMs = options.IdleTimeout != Timeout.InfiniteTimeSpan + ? (ulong)options.IdleTimeout.TotalMilliseconds + : 0; // 0 disables the timeout } - if (options.KeepAliveInterval != Timeout.InfiniteTimeSpan) - { - settings.IsSet.KeepAliveIntervalMs = 1; - settings.KeepAliveIntervalMs = (uint)options.KeepAliveInterval.TotalMilliseconds; - } + 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.InitialReceiveWindowSizes.Connection; @@ -148,6 +149,11 @@ private static unsafe MsQuicSafeHandle Create(QuicConnectionOptions options, QUI settings.IsSet.StreamRecvWindowUnidiDefault = 1; settings.StreamRecvWindowUnidiDefault = (uint)options.InitialReceiveWindowSizes.UnidirectionalStream; + settings.IsSet.HandshakeIdleTimeoutMs = 1; + settings.HandshakeIdleTimeoutMs = options.HandshakeTimeout != Timeout.InfiniteTimeSpan + ? (ulong)options.HandshakeTimeout.TotalMilliseconds + : 0; // 0 disables the timeout + QUIC_HANDLE* handle; using MsQuicBuffers msquicBuffers = new MsQuicBuffers(); 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 550d68dbfb65a3..fd4622f27d9682 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 @@ -117,7 +117,7 @@ public QuicReceiveWindowSizes InitialReceiveWindowSizes /// The upper bound on time when the handshake must complete. If the handshake does not /// complete in this time, the connection is aborted. /// - public TimeSpan HandshakeTimeout { get; set; } = TimeSpan.FromSeconds(10); + public TimeSpan HandshakeTimeout { get; set; } = QuicDefaults.HandshakeTimeout; /// /// Validates the options and potentially sets platform specific defaults. 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 4715effc5dbaf2..d5557b35545430 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,7 @@ 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 (same default as MsQuic) /// public static readonly TimeSpan HandshakeTimeout = TimeSpan.FromSeconds(10); } 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 8ecbfb9901eb25..177fec42c49b15 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 @@ -210,15 +210,24 @@ 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 linkec CTS is cancelled + TimeSpan handshakeTimeout = TimeSpan.Zero; try { using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_disposeCts.Token); - linkedCts.CancelAfter(QuicDefaults.HandshakeTimeout); cancellationToken = linkedCts.Token; + 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)); + handshakeTimeout = options.HandshakeTimeout; + linkedCts.CancelAfter(handshakeTimeout); + await connection.FinishHandshakeAsync(options, clientHello.ServerName, cancellationToken).ConfigureAwait(false); if (!_acceptQueue.Writer.TryWrite(connection)) { @@ -241,7 +250,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 +259,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)) { From 6d34b07f1c6959d93d0d57ac346875049c4ca877 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Wed, 15 Nov 2023 19:30:27 +0100 Subject: [PATCH 5/8] Use Connect timeout on client as well --- .../src/System/Net/Quic/QuicConnection.cs | 14 +++++++++++++- .../src/System/Net/Quic/QuicListener.cs | 5 ++++- 2 files changed, 17 insertions(+), 2 deletions(-) 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 24fb799b51f976..344f1a786a70d6 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,21 @@ public static ValueTask ConnectAsync(QuicClientConnectionOptions static async ValueTask StartConnectAsync(QuicClientConnectionOptions options, CancellationToken cancellationToken) { QuicConnection connection = new QuicConnection(); + + using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + 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) + { + // connect timeout elapsed, since handshake is not done yet, application error code + // is not applied yet. + await connection.DisposeAsync().ConfigureAwait(false); + + throw new QuicException(QuicError.ConnectionTimeout, null, SR.Format(SR.net_quic_handshake_timeout, options.HandshakeTimeout)); } catch { 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 177fec42c49b15..c4d1df98ff36ce 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 @@ -214,17 +214,20 @@ private async void StartConnectionHandshake(QuicConnection connection, SslClient // 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 linkec CTS is cancelled - TimeSpan handshakeTimeout = TimeSpan.Zero; + TimeSpan handshakeTimeout = QuicDefaults.HandshakeTimeout; try { using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_disposeCts.Token); cancellationToken = linkedCts.Token; + linkedCts.CancelAfter(handshakeTimeout); wrapException = true; QuicServerConnectionOptions options = await _connectionOptionsCallback(connection, clientHello, cancellationToken).ConfigureAwait(false); wrapException = false; options.Validate(nameof(options)); + + // update handshake timetout based on the returned value handshakeTimeout = options.HandshakeTimeout; linkedCts.CancelAfter(handshakeTimeout); From 0ae784106809588b1ccfe67bc8f5b7f14bf67246 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Thu, 16 Nov 2023 14:01:21 +0100 Subject: [PATCH 6/8] Code review feedback --- .../src/System/Net/Quic/QuicConnection.cs | 4 +- .../System/Net/Quic/QuicConnectionOptions.cs | 14 ++++--- .../src/System/Net/Quic/QuicDefaults.cs | 10 +++++ .../src/System/Net/Quic/QuicListener.cs | 3 +- .../FunctionalTests/QuicListenerTests.cs | 38 ++++++++++++++----- 5 files changed, 52 insertions(+), 17 deletions(-) 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 344f1a786a70d6..847d85a3e11df0 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 @@ -76,8 +76,8 @@ static async ValueTask StartConnectAsync(QuicClientConnectionOpt } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { - // connect timeout elapsed, since handshake is not done yet, application error code - // is not applied yet. + // 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)); 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 fd4622f27d9682..13f5393d89d39f 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 @@ -8,29 +8,29 @@ namespace System.Net.Quic; /// -/// Collection of receive window sizes for as a whole and individual types. +/// 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; } = 16 * 1024 * 1024; + public int Connection { get; set; } = QuicDefaults.DefaultConnectionMaxData; /// /// The initial flow-control window size for locally initiated bidirectional streams. /// - public int LocallyInitiatedBidirectionalStream { get; set; } = 64 * 1024; + public int LocallyInitiatedBidirectionalStream { get; set; } = QuicDefaults.DefaultStreamMaxData; /// /// The initial flow-control window size for remotely initiated bidirectional streams. /// - public int RemotelyInitiatedBidirectionalStream { get; set; } = 64 * 1024; + public int RemotelyInitiatedBidirectionalStream { get; set; } = QuicDefaults.DefaultStreamMaxData; /// /// The initial flow-control window size for (remotely initiated) unidirectional streams. /// - public int UnidirectionalStream { get; set; } = 64 * 1024; + public int UnidirectionalStream { get; set; } = QuicDefaults.DefaultStreamMaxData; internal void Validate(string argumentName) { @@ -110,12 +110,16 @@ public QuicReceiveWindowSizes InitialReceiveWindowSizes /// /// 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; 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 d5557b35545430..3a1fef8568f277 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 @@ -37,4 +37,14 @@ internal static partial class QuicDefaults /// Default handshake timeout (same default as MsQuic) /// 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 c4d1df98ff36ce..fb67ef89e75498 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 @@ -213,12 +213,13 @@ private async void StartConnectionHandshake(QuicConnection connection, SslClient // 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 linkec CTS is cancelled + // This will be assigned to before the linked CTS is cancelled TimeSpan handshakeTimeout = QuicDefaults.HandshakeTimeout; try { using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_disposeCts.Token); cancellationToken = linkedCts.Token; + // initial timeout for retrieving connection options linkedCts.CancelAfter(handshakeTimeout); wrapException = true; diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs index b63ee10c5834f2..4ae612e05f34a7 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,31 @@ 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); - Assert.Contains(TlsAlertMessage.UserCanceled.ToString(), connectException.Message); + 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] From dee37af75d17ab4ac7929ab425c06f3d522634f4 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Thu, 16 Nov 2023 14:57:45 +0100 Subject: [PATCH 7/8] Cancel ServerOptionsCallback when client closes connection --- .../src/System/Net/Quic/QuicConnection.cs | 4 +++ .../src/System/Net/Quic/QuicListener.cs | 25 ++++++++++++++- .../FunctionalTests/QuicListenerTests.cs | 31 ++++++++++++++++++- 3 files changed, 58 insertions(+), 2 deletions(-) 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 847d85a3e11df0..3b2230fcbc572b 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 @@ -105,6 +105,9 @@ static async ValueTask StartConnectAsync(QuicClientConnectionOpt private readonly ValueTaskSource _connectedTcs = new ValueTaskSource(); private readonly ValueTaskSource _shutdownTcs = new ValueTaskSource(); + private readonly CancellationTokenSource _shutdownCancellationTokenSource = new CancellationTokenSource(); + internal CancellationToken ShutdownCancellationToken => _shutdownCancellationTokenSource.Token; + private readonly Channel _acceptQueue = Channel.CreateUnbounded(new UnboundedChannelOptions() { SingleWriter = true @@ -509,6 +512,7 @@ private unsafe int HandleEventShutdownComplete() _acceptQueue.Writer.TryComplete(exception); _connectedTcs.TrySetException(exception); _shutdownTcs.TrySetResult(); + _shutdownCancellationTokenSource.Cancel(); return QUIC_STATUS_SUCCESS; } private unsafe int HandleEventLocalAddressChanged(ref LOCAL_ADDRESS_CHANGED_DATA data) 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 fb67ef89e75498..d7c3f4d4c38218 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; @@ -217,7 +218,7 @@ private async void StartConnectionHandshake(QuicConnection connection, SslClient TimeSpan handshakeTimeout = QuicDefaults.HandshakeTimeout; try { - using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_disposeCts.Token); + using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_disposeCts.Token, connection.ShutdownCancellationToken); cancellationToken = linkedCts.Token; // initial timeout for retrieving connection options linkedCts.CancelAfter(handshakeTimeout); @@ -239,6 +240,28 @@ private async void StartConnectionHandshake(QuicConnection connection, SslClient await connection.DisposeAsync().ConfigureAwait(false); } } + catch (OperationCanceledException) when (connection.ShutdownCancellationToken.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: diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs index 4ae612e05f34a7..56767f72a82c9a 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs @@ -185,6 +185,35 @@ public async Task AcceptConnectionAsync_SlowOptionsCallback_TimesOut(bool useCan } } + [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] public async Task AcceptConnectionAsync_ListenerDisposed_Throws() { @@ -372,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] From f666277b1a98da24d52b63d14cb538677b979f57 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Mon, 27 Nov 2023 13:54:55 +0100 Subject: [PATCH 8/8] Code review feedback --- .../src/System/Net/Quic/Internal/MsQuicApi.cs | 1 - .../Net/Quic/Internal/MsQuicConfiguration.cs | 30 +++++++++++-------- .../src/System/Net/Quic/QuicConnection.cs | 15 +++++++--- .../System/Net/Quic/QuicConnectionOptions.cs | 2 +- .../src/System/Net/Quic/QuicDefaults.cs | 2 +- .../src/System/Net/Quic/QuicListener.cs | 4 +-- 6 files changed, 33 insertions(+), 21 deletions(-) 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 f4dfb68376acec..e89119844c744c 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 @@ -18,7 +18,6 @@ internal sealed unsafe partial class MsQuicApi { private static readonly Version s_minWindowsVersion = new Version(10, 0, 20145, 1000); - // TODO: update after https://github.com/microsoft/msquic/pull/3948 is released private static readonly Version s_minMsQuicVersion = new Version(2, 2, 2); private static readonly delegate* unmanaged[Cdecl] MsQuicOpenVersion; 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 5c4f61cd6791a0..3837e8984cfa81 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 @@ -132,27 +132,33 @@ private static unsafe MsQuicSafeHandle Create(QuicConnectionOptions options, QUI : 0; // 0 disables the timeout } - settings.IsSet.KeepAliveIntervalMs = 1; - settings.KeepAliveIntervalMs = options.KeepAliveInterval != Timeout.InfiniteTimeSpan - ? (uint)options.KeepAliveInterval.TotalMilliseconds - : 0; // 0 disables the keepalive + 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.InitialReceiveWindowSizes.Connection; + settings.ConnFlowControlWindow = (uint)(options._initialRecieveWindowSizes?.Connection ?? QuicDefaults.DefaultConnectionMaxData); settings.IsSet.StreamRecvWindowBidiLocalDefault = 1; - settings.StreamRecvWindowBidiLocalDefault = (uint)options.InitialReceiveWindowSizes.LocallyInitiatedBidirectionalStream; + settings.StreamRecvWindowBidiLocalDefault = (uint)(options._initialRecieveWindowSizes?.LocallyInitiatedBidirectionalStream ?? QuicDefaults.DefaultStreamMaxData); settings.IsSet.StreamRecvWindowBidiRemoteDefault = 1; - settings.StreamRecvWindowBidiRemoteDefault = (uint)options.InitialReceiveWindowSizes.RemotelyInitiatedBidirectionalStream; + settings.StreamRecvWindowBidiRemoteDefault = (uint)(options._initialRecieveWindowSizes?.RemotelyInitiatedBidirectionalStream ?? QuicDefaults.DefaultStreamMaxData); settings.IsSet.StreamRecvWindowUnidiDefault = 1; - settings.StreamRecvWindowUnidiDefault = (uint)options.InitialReceiveWindowSizes.UnidirectionalStream; + settings.StreamRecvWindowUnidiDefault = (uint)(options._initialRecieveWindowSizes?.UnidirectionalStream ?? QuicDefaults.DefaultStreamMaxData); - settings.IsSet.HandshakeIdleTimeoutMs = 1; - settings.HandshakeIdleTimeoutMs = options.HandshakeTimeout != Timeout.InfiniteTimeSpan - ? (ulong)options.HandshakeTimeout.TotalMilliseconds - : 0; // 0 disables the timeout + 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/QuicConnection.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs index 3b2230fcbc572b..be4a8f8f639b00 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 @@ -68,7 +68,11 @@ static async ValueTask StartConnectAsync(QuicClientConnectionOpt QuicConnection connection = new QuicConnection(); using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - linkedCts.CancelAfter(options.HandshakeTimeout); + + if (options.HandshakeTimeout != Timeout.InfiniteTimeSpan && options.HandshakeTimeout != TimeSpan.Zero) + { + linkedCts.CancelAfter(options.HandshakeTimeout); + } try { @@ -105,8 +109,10 @@ static async ValueTask StartConnectAsync(QuicClientConnectionOpt private readonly ValueTaskSource _connectedTcs = new ValueTaskSource(); private readonly ValueTaskSource _shutdownTcs = new ValueTaskSource(); - private readonly CancellationTokenSource _shutdownCancellationTokenSource = new CancellationTokenSource(); - internal CancellationToken ShutdownCancellationToken => _shutdownCancellationTokenSource.Token; + 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() { @@ -511,8 +517,8 @@ 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(); - _shutdownCancellationTokenSource.Cancel(); return QUIC_STATUS_SUCCESS; } private unsafe int HandleEventLocalAddressChanged(ref LOCAL_ADDRESS_CHANGED_DATA data) @@ -633,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 13f5393d89d39f..81a20eacad9513 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 @@ -97,7 +97,7 @@ internal QuicConnectionOptions() // We can safely use this to distinguish if user provided value during validation. public long DefaultCloseErrorCode { get; set; } = -1; - private QuicReceiveWindowSizes? _initialRecieveWindowSizes; + internal QuicReceiveWindowSizes? _initialRecieveWindowSizes; /// /// The initial receive window sizes for the connection and individual stream types. 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 3a1fef8568f277..e31bc1d21c20dd 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,7 @@ internal static partial class QuicDefaults public const long MaxErrorCodeValue = (1L << 62) - 1; /// - /// Default handshake timeout (same default as MsQuic) + /// Default handshake timeout. /// public static readonly TimeSpan HandshakeTimeout = TimeSpan.FromSeconds(10); 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 d7c3f4d4c38218..e39d718718d118 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 @@ -218,7 +218,7 @@ private async void StartConnectionHandshake(QuicConnection connection, SslClient TimeSpan handshakeTimeout = QuicDefaults.HandshakeTimeout; try { - using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_disposeCts.Token, connection.ShutdownCancellationToken); + using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_disposeCts.Token, connection.ConnectionShutdownToken); cancellationToken = linkedCts.Token; // initial timeout for retrieving connection options linkedCts.CancelAfter(handshakeTimeout); @@ -240,7 +240,7 @@ private async void StartConnectionHandshake(QuicConnection connection, SslClient await connection.DisposeAsync().ConfigureAwait(false); } } - catch (OperationCanceledException) when (connection.ShutdownCancellationToken.IsCancellationRequested) + catch (OperationCanceledException) when (connection.ConnectionShutdownToken.IsCancellationRequested) { // Connection closed by peer if (NetEventSource.Log.IsEnabled())