From caab4832293180e836eead4ff9a3b09f0bc6d4b3 Mon Sep 17 00:00:00 2001 From: Tomas Weinfurt Date: Thu, 23 May 2024 07:58:26 -0700 Subject: [PATCH] basic support for TCP fast open (#99490) * initial drop * update * cleanup * err * 'test' * windows * sync * feedback & updates * macos * windows * feedback * await * docs * note * macos --------- Co-authored-by: Ubuntu --- .../Unix/System.Native/Interop.Connect.cs | 3 + .../ref/System.Net.Sockets.cs | 1 + .../src/System.Net.Sockets.csproj | 8 +- .../Net/Sockets/SafeSocketHandle.Unix.cs | 7 +- .../Net/Sockets/SocketAsyncContext.Unix.cs | 66 +++++-- .../Net/Sockets/SocketAsyncEventArgs.Unix.cs | 19 +- .../System/Net/Sockets/SocketOptionName.cs | 24 ++- .../src/System/Net/Sockets/SocketPal.Unix.cs | 39 +++- .../tests/FunctionalTests/Connect.cs | 1 - .../SocketAsyncEventArgsTest.cs | 184 +++++++++++++++++- .../FunctionalTests/SocketOptionNameTest.cs | 33 ++++ src/native/libs/Common/pal_config.h.in | 1 + src/native/libs/System.Native/entrypoints.c | 1 + .../libs/System.Native/pal_networking.c | 51 +++++ .../libs/System.Native/pal_networking.h | 3 + src/native/libs/configure.cmake | 5 + 16 files changed, 419 insertions(+), 27 deletions(-) diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Connect.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Connect.cs index 11d4e8d0da0ff1..b78f1ea1daecca 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Connect.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Connect.cs @@ -10,5 +10,8 @@ internal static partial class Sys { [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_Connect")] internal static unsafe partial Error Connect(SafeHandle socket, byte* socketAddress, int socketAddressLen); + + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_Connectx")] + internal static unsafe partial Error Connectx(SafeHandle socket, byte* socketAddress, int socketAddressLen, Span buffer, int bufferLen, int enableTfo, int* sent); } } diff --git a/src/libraries/System.Net.Sockets/ref/System.Net.Sockets.cs b/src/libraries/System.Net.Sockets/ref/System.Net.Sockets.cs index fca21b5d7c9522..cac94cac46a8b1 100644 --- a/src/libraries/System.Net.Sockets/ref/System.Net.Sockets.cs +++ b/src/libraries/System.Net.Sockets/ref/System.Net.Sockets.cs @@ -568,6 +568,7 @@ public enum SocketOptionName DropMembership = 13, DontFragment = 14, AddSourceMembership = 15, + FastOpen = 15, DontRoute = 16, DropSourceMembership = 16, TcpKeepAliveRetryCount = 16, diff --git a/src/libraries/System.Net.Sockets/src/System.Net.Sockets.csproj b/src/libraries/System.Net.Sockets/src/System.Net.Sockets.csproj index 39ef6f50620213..ced52a7f14f7e3 100644 --- a/src/libraries/System.Net.Sockets/src/System.Net.Sockets.csproj +++ b/src/libraries/System.Net.Sockets/src/System.Net.Sockets.csproj @@ -1,7 +1,7 @@ - $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-unix;$(NetCoreAppCurrent) + $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-unix;$(NetCoreAppCurrent)-osx;$(NetCoreAppCurrent)-ios;$(NetCoreAppCurrent)-tvos;$(NetCoreAppCurrent) true @@ -13,6 +13,8 @@ $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) SR.SystemNetSockets_PlatformNotSupported + true + $(DefineConstants);SYSTEM_NET_SOCKETS_APPLE_PLATFROM @@ -181,7 +183,7 @@ Link="Common\System\Net\CompletionPortHelper.Windows.cs" /> - + @@ -301,7 +303,7 @@ - + diff --git a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SafeSocketHandle.Unix.cs b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SafeSocketHandle.Unix.cs index 35e9871bb88a68..e3d609c69483c9 100644 --- a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SafeSocketHandle.Unix.cs +++ b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SafeSocketHandle.Unix.cs @@ -23,7 +23,9 @@ public partial class SafeSocketHandle internal bool ExposedHandleOrUntrackedConfiguration { get; private set; } internal bool PreferInlineCompletions { get; set; } = SocketAsyncEngine.InlineSocketCompletionsEnabled; internal bool IsSocket { get; set; } = true; // (ab)use Socket class for performing async I/O on non-socket fds. - +#if SYSTEM_NET_SOCKETS_APPLE_PLATFROM + internal bool TfoEnabled { get; set; } +#endif internal void RegisterConnectResult(SocketError error) { switch (error) @@ -44,6 +46,9 @@ internal void TransferTrackedState(SafeSocketHandle target) target.DualMode = DualMode; target.ExposedHandleOrUntrackedConfiguration = ExposedHandleOrUntrackedConfiguration; target.IsSocket = IsSocket; +#if SYSTEM_NET_SOCKETS_APPLE_PLATFROM + target.TfoEnabled = TfoEnabled; +#endif } internal void SetExposed() => ExposedHandleOrUntrackedConfiguration = true; diff --git a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncContext.Unix.cs b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncContext.Unix.cs index 58a806d6d68ea0..4ca023232b1a5b 100644 --- a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncContext.Unix.cs +++ b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncContext.Unix.cs @@ -362,7 +362,7 @@ public override unsafe void InvokeCallback(bool allowPooling) => Callback!(BytesTransferred, SocketAddress, SocketFlags.None, ErrorCode); } - private sealed class BufferMemorySendOperation : SendOperation + private class BufferMemorySendOperation : SendOperation { public Memory Buffer; @@ -648,21 +648,47 @@ public override void InvokeCallback(bool allowPooling) } } - private sealed class ConnectOperation : WriteOperation + private sealed class ConnectOperation : BufferMemorySendOperation { public ConnectOperation(SocketAsyncContext context) : base(context) { } - public Action? Callback { get; set; } - protected override bool DoTryComplete(SocketAsyncContext context) { bool result = SocketPal.TryCompleteConnect(context._socket, out ErrorCode); context._socket.RegisterConnectResult(ErrorCode); + + if (result && ErrorCode == SocketError.Success && Buffer.Length > 0) + { + SocketError error = context.SendToAsync(Buffer, 0, Buffer.Length, SocketFlags.None, Memory.Empty, ref BytesTransferred, Callback!, default); + if (error != SocketError.Success && error != SocketError.IOPending) + { + context._socket.RegisterConnectResult(ErrorCode); + } + } return result; } - public override void InvokeCallback(bool allowPooling) => - Callback!(ErrorCode); + public override unsafe void InvokeCallback(bool allowPooling) + { + var cb = Callback!; + int bt = BytesTransferred; + Memory sa = SocketAddress; + SocketError ec = ErrorCode; + Memory buffer = Buffer; + + if (allowPooling) + { + AssociatedContext.ReturnOperation(this); + } + + if (buffer.Length == 0) + { + // Invoke callback only when we are completely done. + // In case data were provided for Connect we may or may not send them all. + // If we did not we will need follow-up with Send operation + cb(bt, sa, SocketFlags.None, ec); + } + } } private sealed class SendFileOperation : WriteOperation @@ -1478,7 +1504,6 @@ public SocketError AcceptAsync(Memory socketAddress, out int socketAddress public SocketError Connect(Memory socketAddress) { Debug.Assert(socketAddress.Length > 0, $"Unexpected socketAddressLen: {socketAddress.Length}"); - // Connect is different than the usual "readiness" pattern of other operations. // We need to call TryStartConnect to initiate the connect with the OS, // before we try to complete it via epoll notification. @@ -1503,7 +1528,7 @@ public SocketError Connect(Memory socketAddress) return operation.ErrorCode; } - public SocketError ConnectAsync(Memory socketAddress, Action callback) + public SocketError ConnectAsync(Memory socketAddress, Action, SocketFlags, SocketError> callback, Memory buffer, out int sentBytes) { Debug.Assert(socketAddress.Length > 0, $"Unexpected socketAddressLen: {socketAddress.Length}"); Debug.Assert(callback != null, "Expected non-null callback"); @@ -1516,9 +1541,20 @@ public SocketError ConnectAsync(Memory socketAddress, Action SocketError errorCode; int observedSequenceNumber; _sendQueue.IsReady(this, out observedSequenceNumber); - if (SocketPal.TryStartConnect(_socket, socketAddress, out errorCode)) +#if SYSTEM_NET_SOCKETS_APPLE_PLATFROM + if (SocketPal.TryStartConnect(_socket, socketAddress, out errorCode, buffer.Span, _socket.TfoEnabled, out sentBytes)) +#else + if (SocketPal.TryStartConnect(_socket, socketAddress, out errorCode, buffer.Span, false, out sentBytes)) // In Linux, we can figure it out as needed inside PAL. +#endif { _socket.RegisterConnectResult(errorCode); + + int remains = buffer.Length - sentBytes; + + if (errorCode == SocketError.Success && remains > 0) + { + errorCode = SendToAsync(buffer.Slice(sentBytes), 0, remains, SocketFlags.None, Memory.Empty, ref sentBytes, callback!, default); + } return errorCode; } @@ -1526,10 +1562,16 @@ public SocketError ConnectAsync(Memory socketAddress, Action { Callback = callback, SocketAddress = socketAddress, + Buffer = buffer.Slice(sentBytes), + BytesTransferred = sentBytes, }; if (!_sendQueue.StartAsyncOperation(this, operation, observedSequenceNumber)) { + if (operation.ErrorCode == SocketError.Success) + { + sentBytes += operation.BytesTransferred; + } return operation.ErrorCode; } @@ -1880,7 +1922,8 @@ public SocketError Send(byte[] buffer, int offset, int count, SocketFlags flags, public SocketError SendAsync(Memory buffer, int offset, int count, SocketFlags flags, out int bytesSent, Action, SocketFlags, SocketError> callback, CancellationToken cancellationToken) { - return SendToAsync(buffer, offset, count, flags, Memory.Empty, out bytesSent, callback, cancellationToken); + bytesSent = 0; + return SendToAsync(buffer, offset, count, flags, Memory.Empty, ref bytesSent, callback, cancellationToken); } public SocketError SendTo(byte[] buffer, int offset, int count, SocketFlags flags, Memory socketAddress, int timeout, out int bytesSent) @@ -1947,11 +1990,10 @@ public unsafe SocketError SendTo(ReadOnlySpan buffer, SocketFlags flags, M } } - public SocketError SendToAsync(Memory buffer, int offset, int count, SocketFlags flags, Memory socketAddress, out int bytesSent, Action, SocketFlags, SocketError> callback, CancellationToken cancellationToken = default) + public SocketError SendToAsync(Memory buffer, int offset, int count, SocketFlags flags, Memory socketAddress, ref int bytesSent, Action, SocketFlags, SocketError> callback, CancellationToken cancellationToken = default) { SetHandleNonBlocking(); - bytesSent = 0; SocketError errorCode; int observedSequenceNumber; if (_sendQueue.IsReady(this, out observedSequenceNumber) && diff --git a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncEventArgs.Unix.cs b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncEventArgs.Unix.cs index b88eb1375cf919..bcb411922ae05e 100644 --- a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncEventArgs.Unix.cs +++ b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncEventArgs.Unix.cs @@ -67,17 +67,24 @@ internal unsafe SocketError DoOperationAccept(Socket _ /*socket*/, SafeSocketHan return socketError; } - private void ConnectCompletionCallback(SocketError socketError) + private void ConnectCompletionCallback(int bytesTransferred, Memory socketAddress, SocketFlags receivedFlags, SocketError socketError) { - CompletionCallback(0, SocketFlags.None, socketError); + CompletionCallback(bytesTransferred, SocketFlags.None, socketError); } internal unsafe SocketError DoOperationConnectEx(Socket _ /*socket*/, SafeSocketHandle handle) - => DoOperationConnect(handle); + { + SocketError socketError = handle.AsyncContext.ConnectAsync(_socketAddress!.Buffer, ConnectCompletionCallback, _buffer.Slice(_offset, _count), out int sentBytes); + if (socketError != SocketError.IOPending) + { + FinishOperationSync(socketError, sentBytes, SocketFlags.None); + } + return socketError; + } internal unsafe SocketError DoOperationConnect(SafeSocketHandle handle) { - SocketError socketError = handle.AsyncContext.ConnectAsync(_socketAddress!.Buffer, ConnectCompletionCallback); + SocketError socketError = handle.AsyncContext.ConnectAsync(_socketAddress!.Buffer, ConnectCompletionCallback, Memory.Empty, out int _); if (socketError != SocketError.IOPending) { FinishOperationSync(socketError, 0, SocketFlags.None); @@ -299,11 +306,11 @@ internal SocketError DoOperationSendTo(SafeSocketHandle handle, CancellationToke _receivedFlags = System.Net.Sockets.SocketFlags.None; _socketAddressSize = 0; - int bytesSent; + int bytesSent = 0; SocketError errorCode; if (_bufferList == null) { - errorCode = handle.AsyncContext.SendToAsync(_buffer, _offset, _count, _socketFlags, _socketAddress!.Buffer, out bytesSent, TransferCompletionCallback, cancellationToken); + errorCode = handle.AsyncContext.SendToAsync(_buffer, _offset, _count, _socketFlags, _socketAddress!.Buffer, ref bytesSent, TransferCompletionCallback, cancellationToken); } else { diff --git a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketOptionName.cs b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketOptionName.cs index 3b91469bf76635..226297ece51479 100644 --- a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketOptionName.cs +++ b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketOptionName.cs @@ -132,12 +132,34 @@ public enum SocketOptionName #endregion #region SocketOptionLevel.Tcp - // Disables the Nagle algorithm for send coalescing. + /// + /// Disables the Nagle algorithm for send coalescing. + /// NoDelay = 1, + /// + /// Use urgent data as defined in RFC-1222. This option can be set only once; after it is set, it cannot be turned off. + /// BsdUrgent = 2, + /// + /// Use expedited data as defined in RFC-1222. This option can be set only once; after it is set, it cannot be turned off. + /// Expedited = 2, + /// + /// This enables TCP Fast Open as defined in RFC-7413. The actual observed behavior depend on OS configuration and state of kernel TCP cookie cache. + /// Enabling TFO can impact interoperability and casue connectivity issues. + /// + FastOpen = 15, + /// + /// The number of TCP keep alive probes that will be sent before the connection is terminated. + /// TcpKeepAliveRetryCount = 16, + /// + /// The number of seconds a TCP connection will remain alive/idle before keepalive probes are sent to the remote. + /// TcpKeepAliveTime = 3, + /// + /// The number of seconds a TCP connection will wait for a keepalive response before sending another keepalive probe. + /// TcpKeepAliveInterval = 17, #endregion diff --git a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs index 837743dfa34478..26ee3ccb885b91 100644 --- a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs +++ b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs @@ -647,9 +647,12 @@ public static unsafe bool TryCompleteAccept(SafeSocketHandle socket, Memory socketAddress, out SocketError errorCode) + public static unsafe bool TryStartConnect(SafeSocketHandle socket, Memory socketAddress, out SocketError errorCode) => TryStartConnect(socket, socketAddress, out errorCode, Span.Empty, false, out int _ ); + + public static unsafe bool TryStartConnect(SafeSocketHandle socket, Memory socketAddress, out SocketError errorCode, Span data, bool tfo, out int sent) { Debug.Assert(socketAddress.Length > 0, $"Unexpected socketAddressLen: {socketAddress.Length}"); + sent = 0; if (socket.IsDisconnected) { @@ -660,7 +663,16 @@ public static unsafe bool TryStartConnect(SafeSocketHandle socket, Memory Interop.Error err; fixed (byte* rawSocketAddress = socketAddress.Span) { - err = Interop.Sys.Connect(socket, rawSocketAddress, socketAddress.Length); + if (data.Length > 0) + { + int sentBytes = 0; + err = Interop.Sys.Connectx(socket, rawSocketAddress, socketAddress.Length, data, data.Length, tfo ? 1 : 0, &sentBytes); + sent = sentBytes; + } + else + { + err = Interop.Sys.Connect(socket, rawSocketAddress, socketAddress.Length); + } } if (err == Interop.Error.SUCCESS) @@ -1451,6 +1463,18 @@ public static unsafe SocketError SetSockOpt(SafeSocketHandle handle, SocketOptio } } +#if SYSTEM_NET_SOCKETS_APPLE_PLATFROM + // macOS fails to even query it if socket is not actively listening. + // To provide consistent platform experience we will track if + // it was ret and we will use it later as needed. + if (optionLevel == SocketOptionLevel.Tcp && optionName == SocketOptionName.FastOpen) + { + handle.TfoEnabled = optionValue != 0; + // Silently ignore errors - TFO is best effort and it may be disabled by configuration or not + // supported by OS. + err = Interop.Error.SUCCESS; + } +#endif return GetErrorAndTrackSetting(handle, optionLevel, optionName, err); } @@ -1580,6 +1604,17 @@ public static unsafe SocketError GetSockOpt(SafeSocketHandle handle, SocketOptio int optLen = sizeof(int); Interop.Error err = Interop.Sys.GetSockOpt(handle, optionLevel, optionName, (byte*)&value, &optLen); +#if SYSTEM_NET_SOCKETS_APPLE_PLATFROM + // macOS fails to even query it if socket is not actively listening. + // To provide consistent platform experience we will track if + // it was set and we will use it later as needed. + if (optionLevel == SocketOptionLevel.Tcp && optionName == SocketOptionName.FastOpen && err != Interop.Error.SUCCESS) + { + value = handle.TfoEnabled ? 1 : 0; + err = Interop.Error.SUCCESS; + } +#endif + optionValue = value; return err == Interop.Error.SUCCESS ? SocketError.Success : GetSocketErrorForErrorCode(err); } diff --git a/src/libraries/System.Net.Sockets/tests/FunctionalTests/Connect.cs b/src/libraries/System.Net.Sockets/tests/FunctionalTests/Connect.cs index bc2ca6fff5c242..dda67f84155459 100644 --- a/src/libraries/System.Net.Sockets/tests/FunctionalTests/Connect.cs +++ b/src/libraries/System.Net.Sockets/tests/FunctionalTests/Connect.cs @@ -266,7 +266,6 @@ public sealed class ConnectEap : Connect public ConnectEap(ITestOutputHelper output) : base(output) {} [Theory] - [PlatformSpecific(TestPlatforms.Windows)] [InlineData(true)] [InlineData(false)] public async Task ConnectAsync_WithData_DataReceived(bool useArrayApi) diff --git a/src/libraries/System.Net.Sockets/tests/FunctionalTests/SocketAsyncEventArgsTest.cs b/src/libraries/System.Net.Sockets/tests/FunctionalTests/SocketAsyncEventArgsTest.cs index 3d865cb864570f..59f0dd8b5fa4c3 100644 --- a/src/libraries/System.Net.Sockets/tests/FunctionalTests/SocketAsyncEventArgsTest.cs +++ b/src/libraries/System.Net.Sockets/tests/FunctionalTests/SocketAsyncEventArgsTest.cs @@ -10,6 +10,7 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using Microsoft.DotNet.XUnitExtensions; using Xunit; namespace System.Net.Sockets.Tests @@ -424,6 +425,150 @@ public void CancelConnectAsync_StaticConnect_CancelsInProgressConnect() } } + [ConditionalTheory] + [InlineData(false, 1)] + [InlineData(false, 10_000)] + [InlineData(true, 1)] // This should fit with SYN flag + [InlineData(true, 10_000)] // This should be too big to fit completely to first packet. + public async Task ConnectAsync_WithData_OK(bool useFastOpen, int size) + { + if (useFastOpen && PlatformDetection.IsWindows && !PlatformDetection.IsWindows10OrLater) + { + // Old Windows versions do not support fast open and SetSocketOption fails with error. + throw new SkipTestException("TCP fast open is not supported"); + } + + using (var listen = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + { + listen.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + listen.Listen(); + + var client = new Socket(SocketType.Stream, ProtocolType.Tcp); + if (useFastOpen) + { + listen.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.FastOpen, 1); + client.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.FastOpen, 1); + } + + var sendBuffer = new byte[size]; + var receiveBuffer = new byte[size * 2]; + Random.Shared.NextBytes(sendBuffer); + + var connectSaea = new SocketAsyncEventArgs(); + var tcs = new TaskCompletionSource(); + connectSaea.Completed += (s, e) => tcs.SetResult(e.SocketError); + connectSaea.RemoteEndPoint = listen.LocalEndPoint; + connectSaea.SetBuffer(sendBuffer, 0, size); + + bool pending = client.ConnectAsync(connectSaea); + if (!pending) tcs.SetResult(connectSaea.SocketError); + Socket serverSocket = await listen.AcceptAsync(); + await tcs.Task; + + Assert.Equal(size, connectSaea.BytesTransferred); + // Close the client so we can get easily check the data on server side + client.Shutdown(SocketShutdown.Send); + + int offset = 0; + int readBytes; + do + { + readBytes = await serverSocket.ReceiveAsync(new Memory(receiveBuffer, offset, receiveBuffer.Length - offset), default); + offset += readBytes; + } + while (readBytes != 0); + Assert.Equal(size, offset); + Assert.True(new ReadOnlySpan(receiveBuffer, 0, offset).SequenceEqual(sendBuffer)); + + serverSocket.Send(new byte[10]); + serverSocket.Close(); + client.Close(); + + // DO second round so TFO has chance to get cookies set up + + tcs = new TaskCompletionSource(); + client = new Socket(SocketType.Stream, ProtocolType.Tcp); + if (useFastOpen) + { + client.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.FastOpen, 1); + } + connectSaea = new SocketAsyncEventArgs(); + connectSaea.Completed += (s, e) => tcs.SetResult(e.SocketError); + connectSaea.RemoteEndPoint = listen.LocalEndPoint; + connectSaea.SetBuffer(new byte[size], 0, size); + + pending = client.ConnectAsync(connectSaea); + if (!pending) tcs.SetResult(connectSaea.SocketError); + serverSocket = await listen.AcceptAsync(); + await tcs.Task; + Assert.Equal(size, connectSaea.BytesTransferred); + await client.SendAsync(new byte[1]); + // Close the client so we can get easily check the data on server side + client.Shutdown(SocketShutdown.Send); + + offset = 0; + readBytes = 0; + do + { + readBytes = await serverSocket.ReceiveAsync(new Memory(receiveBuffer, offset, receiveBuffer.Length - offset), default); + offset += readBytes; + } + while (readBytes != 0); + // We should also get data from the extra Send + Assert.Equal(size + 1, offset); + + serverSocket.Close(); + } + } + + [ConditionalTheory] + [InlineData(false, 1)] + [InlineData(false, 10_000)] + [InlineData(true, 1)] // This should fit with SYN flag + [InlineData(true, 10_000)] // This should be too big to fit completely to first packet. + public async Task Connect_WithData_OK(bool useFastOpen, int size) + { + if (useFastOpen && PlatformDetection.IsWindows && !PlatformDetection.IsWindows10OrLater) + { + // Old Windows versions do not support fast open and SetSocketOption fails with error. + throw new SkipTestException("TCP fast open is not supported"); + } + + using (var listen = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + { + listen.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + listen.Listen(); + + var client = new Socket(SocketType.Stream, ProtocolType.Tcp); + if (useFastOpen) + { + listen.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.FastOpen, 1); + client.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.FastOpen, 1); + } + + var sendBuffer = new byte[size]; + var receiveBuffer = new byte[size * 2]; + Random.Shared.NextBytes(sendBuffer); + + Task serverTask = listen.AcceptAsync(); + // use sync extension + client.Connect(listen.LocalEndPoint, sendBuffer, TestSettings.PassingTestTimeout); + Socket serverSocket = await serverTask; + + client.Shutdown(SocketShutdown.Send); + + int offset = 0; + int readBytes = 0; + do + { + readBytes = await serverSocket.ReceiveAsync(new Memory(receiveBuffer, offset, receiveBuffer.Length - offset), default); + offset += readBytes; + } + while (readBytes != 0); + Assert.Equal(size, offset); + } + } + [Fact] public async Task ReuseSocketAsyncEventArgs_SameInstance_MultipleSockets() { @@ -916,7 +1061,7 @@ public async Task SendTo_DifferentEP_Success(bool ipv4) ArraySegment receiveBuffer = new ArraySegment(receiveInternalBuffer, 0, receiveInternalBuffer.Length); using SocketAsyncEventArgs saea = new SocketAsyncEventArgs(); - ManualResetEventSlim mres = new ManualResetEventSlim(false); + ManualResetEventSlim mres = new ManualResetEventSlim(false); saea.SetBuffer(sendBuffer); saea.RemoteEndPoint = receiver1.LocalEndPoint; @@ -943,4 +1088,41 @@ public async Task SendTo_DifferentEP_Success(bool ipv4) Assert.Equal(sendBuffer.Length, result.ReceivedBytes); } } + + internal static class ConnectExtensions + { + internal static void Connect(this Socket socket, EndPoint ep, Memory buffer, int timeout) + { + var re = new ManualResetEventSlim(); + var saea = new SocketAsyncEventArgs(); + saea.SetBuffer(buffer); + saea.RemoteEndPoint = ep; + saea.Completed += (_, __) => re.Set(); + if (!socket.ConnectAsync(saea)) + { + re.Wait(timeout); + } + } + + internal static Task ConnectAsync(this Socket socket, EndPoint ep, Memory buffer, CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(cancellationToken); + var saea = new SocketAsyncEventArgs(); + saea.SetBuffer(buffer); + saea.RemoteEndPoint = ep; + + saea.Completed += (s, e) => + { + Console.WriteLine("saea.Completed called with {0} transferrred = {1} from {2}", e.SocketError, e.BytesTransferred, buffer.Length); + tcs.SetResult(e.SocketError); + }; + + if (!socket.ConnectAsync(saea)) + { + tcs.SetResult(saea.SocketError); + } + + return tcs.Task; + } + } } diff --git a/src/libraries/System.Net.Sockets/tests/FunctionalTests/SocketOptionNameTest.cs b/src/libraries/System.Net.Sockets/tests/FunctionalTests/SocketOptionNameTest.cs index 2a9c6a24f74be6..8692ecd2329547 100644 --- a/src/libraries/System.Net.Sockets/tests/FunctionalTests/SocketOptionNameTest.cs +++ b/src/libraries/System.Net.Sockets/tests/FunctionalTests/SocketOptionNameTest.cs @@ -417,6 +417,39 @@ public void ExclusiveAddressUseTcp() } } + [ConditionalFact] + public async Task TcpFastOpen_Roundrip_Succeeds() + { + if (PlatformDetection.IsWindows && !PlatformDetection.IsWindows10OrLater) + { + // Old Windows versions do not support fast open and SetSocketOption fails with error. + throw new SkipTestException("TCP fast open is not supported"); + } + + using (Socket l = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + { + l.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + l.Listen(); + + int oldValue = (int)l.GetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.FastOpen); + int newValue = oldValue == 0 ? 1 : 0; + l.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.FastOpen, newValue); + oldValue = (int)l.GetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.FastOpen); + Assert.Equal(newValue, oldValue); + + using (Socket c = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + { + oldValue = (int)c.GetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.FastOpen); + newValue = oldValue == 0 ? 1 : 0; + c.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.FastOpen, newValue); + oldValue = (int)c.GetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.FastOpen); + Assert.Equal(newValue, oldValue); + + await c.ConnectAsync(l.LocalEndPoint); + } + } + } + [Fact] [PlatformSpecific(TestPlatforms.Linux | TestPlatforms.OSX)] public unsafe void ReuseAddressUdp() diff --git a/src/native/libs/Common/pal_config.h.in b/src/native/libs/Common/pal_config.h.in index 22b1d5713acf79..9f409dc3195e88 100644 --- a/src/native/libs/Common/pal_config.h.in +++ b/src/native/libs/Common/pal_config.h.in @@ -130,6 +130,7 @@ #cmakedefine01 HAVE_TCP_H_TCP_KEEPALIVE #cmakedefine01 HAVE_BUILTIN_MUL_OVERFLOW #cmakedefine01 HAVE_DISCONNECTX +#cmakedefine01 HAVE_CONNECTX #cmakedefine01 HAVE_CFSETSPEED #cmakedefine01 HAVE_CFMAKERAW #cmakedefine01 HAVE_GETGROUPLIST diff --git a/src/native/libs/System.Native/entrypoints.c b/src/native/libs/System.Native/entrypoints.c index 30cc86f2aff976..51c761109159b5 100644 --- a/src/native/libs/System.Native/entrypoints.c +++ b/src/native/libs/System.Native/entrypoints.c @@ -166,6 +166,7 @@ static const Entry s_sysNative[] = DllImportEntry(SystemNative_Accept) DllImportEntry(SystemNative_Bind) DllImportEntry(SystemNative_Connect) + DllImportEntry(SystemNative_Connectx) DllImportEntry(SystemNative_GetPeerName) DllImportEntry(SystemNative_GetSockName) DllImportEntry(SystemNative_Listen) diff --git a/src/native/libs/System.Native/pal_networking.c b/src/native/libs/System.Native/pal_networking.c index 9856236627faf3..0deae01d8bff23 100644 --- a/src/native/libs/System.Native/pal_networking.c +++ b/src/native/libs/System.Native/pal_networking.c @@ -1647,6 +1647,53 @@ int32_t SystemNative_Connect(intptr_t socket, uint8_t* socketAddress, int32_t so return err == 0 ? Error_SUCCESS : SystemNative_ConvertErrorPlatformToPal(errno); } +int32_t SystemNative_Connectx(intptr_t socket, uint8_t* socketAddress, int32_t socketAddressLen, uint8_t* data, int32_t dataLen, int32_t tfo, int* sent) +{ + if (socketAddress == NULL || socketAddressLen < 0 || sent == NULL) + { + return Error_EFAULT; + } + + int fd = ToFileDescriptor(socket); +#if HAVE_CONNECTX + struct sa_endpoints eps; + struct iovec iovec; + int flags = 0; + iovec.iov_base = data; + iovec.iov_len = (size_t)dataLen; + memset(&eps, 0, sizeof(eps)); + eps.sae_dstaddr = (struct sockaddr *)socketAddress; + eps.sae_dstaddrlen = (socklen_t)socketAddressLen; + + size_t length = 0; + int err; + while ((err = connectx(fd, &eps, SAE_ASSOCID_ANY, tfo != 0 ? CONNECT_DATA_IDEMPOTENT : 0, dataLen > 0 ? &iovec : NULL, dataLen > 0 ? 1 : 0, &length, NULL)) < 0 && errno == EINTR); + *sent = (int)length; + + return err == 0 ? Error_SUCCESS : SystemNative_ConvertErrorPlatformToPal(errno); +#else +#ifdef TCP_FASTOPEN_CONNECT + int enabled = 1; + socklen_t len = sizeof(enabled); + + // To make it consistent across platform we check if TCP_FASTOPEN and if so we also enabled it for + // TCP_FASTOPEN_CONNECT to avoid platform specific code at Socket layer. + if (getsockopt(fd, IPPROTO_TCP, TCP_FASTOPEN, &enabled, &len) == 0 && enabled != 0) + { + // This will either success and connect will finish without sending SYN until we write to so the socket. + // If this is not available we simply connect and write provided data afterwards. + setsockopt(fd, IPPROTO_TCP, TCP_FASTOPEN_CONNECT, &enabled, len); + } +#endif + // avoid possible warning about unused parameters + (void*)data; + (void)dataLen; + (void)tfo; + sent = 0; + return SystemNative_Connect(socket, socketAddress, socketAddressLen); +#endif +} + int32_t SystemNative_GetPeerName(intptr_t socket, uint8_t* socketAddress, int32_t* socketAddressLen) { if (socketAddress == NULL || socketAddressLen == NULL || *socketAddressLen < 0) @@ -1958,6 +2005,10 @@ static bool TryGetPlatformSocketOption(int32_t socketOptionLevel, int32_t socket *optName = TCP_KEEPINTVL; return true; + case SocketOptionName_SO_TCP_FASTOPEN: + *optName = TCP_FASTOPEN; + return true; + default: return false; } diff --git a/src/native/libs/System.Native/pal_networking.h b/src/native/libs/System.Native/pal_networking.h index 5dfe1c1c54df10..f10f490c5db915 100644 --- a/src/native/libs/System.Native/pal_networking.h +++ b/src/native/libs/System.Native/pal_networking.h @@ -184,6 +184,7 @@ typedef enum SocketOptionName_SO_TCP_KEEPALIVE_RETRYCOUNT = 16, SocketOptionName_SO_TCP_KEEPALIVE_TIME = 3, SocketOptionName_SO_TCP_KEEPALIVE_INTERVAL = 17, + SocketOptionName_SO_TCP_FASTOPEN = 15, // Names for SocketOptionLevel_SOL_UDP // SocketOptionName_SO_UDP_NOCHECKSUM = 1, @@ -370,6 +371,8 @@ PALEXPORT int32_t SystemNative_Bind(intptr_t socket, int32_t protocolType, uint8 PALEXPORT int32_t SystemNative_Connect(intptr_t socket, uint8_t* socketAddress, int32_t socketAddressLen); +PALEXPORT int32_t SystemNative_Connectx(intptr_t socket, uint8_t* socketAddress, int32_t socketAddressLen, uint8_t* data, int32_t dataLen, int32_t tfo, int* sent); + PALEXPORT int32_t SystemNative_GetPeerName(intptr_t socket, uint8_t* socketAddress, int32_t* socketAddressLen); PALEXPORT int32_t SystemNative_GetSockName(intptr_t socket, uint8_t* socketAddress, int32_t* socketAddressLen); diff --git a/src/native/libs/configure.cmake b/src/native/libs/configure.cmake index bba930e0c0072f..d15a5ff74bc96a 100644 --- a/src/native/libs/configure.cmake +++ b/src/native/libs/configure.cmake @@ -531,6 +531,11 @@ check_symbol_exists( "sys/socket.h" HAVE_DISCONNECTX) +check_symbol_exists( + connectx + "sys/socket.h" + HAVE_CONNECTX) + set(PREVIOUS_CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS}) set(CMAKE_REQUIRED_FLAGS "-Werror -Wsign-conversion") check_c_source_compiles(