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]