Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Socket.DuplicateAndClose() for Windows #1858

Merged
merged 18 commits into from
Feb 4, 2020
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Microsoft.Win32.SafeHandles;
using System;
using System.Runtime.InteropServices;

internal static partial class Interop
{
internal static partial class Kernel32
{
[Flags]
internal enum HandleFlags: uint
{
None = 0,
Inherit = 1,
ProtectFromClose = 2
}

[DllImport(Libraries.Kernel32, SetLastError = true)]
internal static extern bool SetHandleInformation(SafeHandle handle, HandleFlags mask, HandleFlags flags);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Net.Sockets;
using System.Runtime.InteropServices;

internal static partial class Interop
{
internal static partial class Winsock
{
[StructLayout(LayoutKind.Sequential)]
internal struct WSAProtocolChain
{
internal int ChainLen;
[MarshalAs(UnmanagedType.ByValArray, SizeConst=7)]
internal uint[] ChainEntries;
}

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)]
internal struct WSAProtocolInfo
{
internal uint ServiceFlags1;
internal uint ServiceFlags2;
internal uint ServiceFlags3;
internal uint ServiceFlags4;
internal uint ProviderFlags;
internal Guid ProviderId;
internal uint CatalogEntryId;
internal WSAProtocolChain ProtocolChain;
internal int Version;
internal AddressFamily AddressFamily;
internal int MaxSockAddr;
internal int MinSockAddr;
internal SocketType SocketType;
internal ProtocolType ProtocolType;
internal int ProtocolMaxOffset;
internal int NetworkByteOrder;
internal int SecurityScheme;
internal uint MessageSize;
internal uint ProviderReserved;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
internal string ProtocolName;

public static readonly int Size = Marshal.SizeOf(typeof(WSAProtocolInfo));
}

[DllImport(Interop.Libraries.Ws2_32, SetLastError = true)]
internal static extern unsafe int WSADuplicateSocket(
[In] SafeSocketHandle socketHandle,
[In] uint targetProcessId,
[In] byte* lpProtocolInfo
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,18 @@ public static void ForceNonBlocking(this Socket socket, bool force)
socket.Blocking = true;
}
}

public static (Socket, Socket) CreateConnectedSocketPair()
{
using Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any potential difference in the handling of different address families? Wondering if we need tests for IPv6 sockets as well, or if Windows doesn't discriminate and a test for IPv4 sockets will be sufficient.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me it looks quite arbitrary whether a functionality is also tested against IPV6 in socket test code. I added a few IPV6 test cases for the duplication to make sure it is covered. See the parameters of DuplicateAndClose_TcpServerHandler which is emulating most common user scenario.

listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));
listener.Listen(1);

Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
client.Connect(listener.LocalEndPoint);
Socket server = listener.Accept();

return (client, server);
}
}
}
6 changes: 6 additions & 0 deletions src/libraries/System.Net.Sockets/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -249,4 +249,10 @@
<data name="net_sockets_valuetaskmisuse" xml:space="preserve">
<value>A ValueTask returned from an asynchronous socket operation was consumed concurrently. ValueTasks must only ever be awaited once. (Id: {0}).</value>
</data>
<data name="net_sockets_invalid_socketinformation" xml:space="preserve">
<value>The specified value for the socket information is invalid.</value>
</data>
<data name="net_sockets_asyncoperations_notallowed" xml:space="preserve">
<value>Asynchronous operations are not allowed on this socket. The underlying OS handle might have been already bound to an IO completion port.</value>
</data>
</root>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>System.Net.Sockets</AssemblyName>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
Expand Down Expand Up @@ -197,6 +197,9 @@
<Compile Include="$(CommonPath)Interop\Windows\WinSock\Interop.WSAConnect.cs">
<Link>Common\Interop\Windows\WinSock\Interop.WSAConnect.cs</Link>
</Compile>
<Compile Include="$(CommonPath)Interop\Windows\WinSock\Interop.WSADuplicateSocket.cs">
<Link>Common\Interop\Windows\WinSock\Interop.WSADuplicateSocket.cs</Link>
</Compile>
<Compile Include="$(CommonPath)Interop\Windows\WinSock\Interop.WSAGetOverlappedResult.cs">
<Link>Common\Interop\Windows\WinSock\Interop.WSAGetOverlappedResult.cs</Link>
</Compile>
Expand Down Expand Up @@ -236,6 +239,9 @@
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.SetFileCompletionNotificationModes.cs">
<Link>Common\Interop\Windows\Kernel32\Interop.SetFileCompletionNotificationModes.cs</Link>
</Compile>
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.HandleInformation.cs">
<Link>Common\Interop\Windows\Kernel32\Interop.HandleInformation.cs</Link>
</Compile>
<Compile Include="$(CommonPath)System\Net\CompletionPortHelper.Windows.cs">
<Link>Common\System\Net\CompletionPortHelper.Windows.cs</Link>
</Compile>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ internal ThreadPoolBoundHandle GetOrAllocateThreadPoolBoundHandle(bool trySkipCo
catch (Exception exception) when (!ExceptionCheck.IsFatal(exception))
{
bool closed = IsClosed;
bool alreadyBound = !IsInvalid && !IsClosed && (exception is ArgumentException);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't we get an ArgumentException as well if the handle wasn't opened for overlapped I/O? We're assuming that's the case then?

Copy link
Member Author

@antonfirsov antonfirsov Feb 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made this based this on my understanding of the code in ClrThreadPoolBoundHandle.Windows.cs:

if (ex.HResult == HResults.E_HANDLE) // Bad handle
throw new ArgumentException(SR.Argument_InvalidHandle, nameof(handle));
if (ex.HResult == HResults.E_INVALIDARG) // Handle already bound or sync handle
throw new ArgumentException(SR.Argument_AlreadyBoundOrSyncHandle, nameof(handle));

I assume that the handle can not be invalid since we checked it before. Based on the comment, and the exception message, the only other possible reason is: "Handle already bound or sync handle". Let me know, if I'm missing something!

CloseAsIs(abortive: false);
if (closed)
{
Expand All @@ -67,6 +68,12 @@ internal ThreadPoolBoundHandle GetOrAllocateThreadPoolBoundHandle(bool trySkipCo
// instead propagate as an ObjectDisposedException.
ThrowSocketDisposedException(exception);
}

if (alreadyBound)
{
throw new InvalidOperationException(SR.net_sockets_asyncoperations_notallowed);
}

throw;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,25 @@ namespace System.Net.Sockets
{
public partial class Socket
{
public Socket(SocketInformation socketInformation)
{
//
// This constructor works in conjunction with DuplicateAndClose, which is not supported on Unix.
// See comments in DuplicateAndClose.
//
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we use this style of commenting elsewhere in System.Net.Sockets, with a blank comment line above and below? We've generally moved away from that elsewhere.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Occasionally yes, it was was used in the methods I changed. Getting rid of it.

throw new PlatformNotSupportedException(SR.net_sockets_duplicateandclose_notsupported);
}

public SocketInformation DuplicateAndClose(int targetProcessId)
{
//
// DuplicateAndClose is not supported on Unix, since passing FD-s between processes
// should involve Unix Domain Sockets. The programming model is fundamentally different,
// and incompatible with the design of SocketInformation API-s.
//
throw new PlatformNotSupportedException(SR.net_sockets_duplicateandclose_notsupported);
}

partial void ValidateForMultiConnect(bool isMultiEndpoint)
{
// ValidateForMultiConnect is called before any {Begin}Connect{Async} call,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,79 @@ public partial class Socket

internal void ReplaceHandleIfNecessaryAfterFailedConnect() { /* nop on Windows */ }

public Socket(SocketInformation socketInformation)
{
if (NetEventSource.IsEnabled) NetEventSource.Enter(this);

InitializeSockets();

SocketError errorCode = SocketPal.CreateSocket(socketInformation, out _handle,
ref _addressFamily, ref _socketType, ref _protocolType);

if (errorCode != SocketError.Success)
{
Debug.Assert(_handle.IsInvalid);

if (errorCode == SocketError.InvalidArgument)
{
throw new ArgumentException(SR.net_sockets_invalid_socketinformation, nameof(socketInformation));
}

// Failed to create the socket, throw.
throw new SocketException((int)errorCode);
}

if (_handle.IsInvalid)
{
Copy link
Member

@stephentoub stephentoub Jan 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should dispose of state here that may have been created, namely the _handle. Even if it's invalid, it'll likely still trigger finalization. Probably want to null it out as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like an unnecessary check, since SocketPal.CreateSocket already validates handle. Added the disposal there, getting rid of this branch here.

throw new SocketException();
}

if (_addressFamily != AddressFamily.InterNetwork && _addressFamily != AddressFamily.InterNetworkV6)
{
throw new NotSupportedException(SR.net_invalidversion);
}

_isConnected = socketInformation.GetOption(SocketInformationOptions.Connected);
_willBlock = !socketInformation.GetOption(SocketInformationOptions.NonBlocking);
InternalSetBlocking(_willBlock);
_isListening = socketInformation.GetOption(SocketInformationOptions.Listening);

IPAddress tempAddress = _addressFamily == AddressFamily.InterNetwork ? IPAddress.Any : IPAddress.IPv6Any;
IPEndPoint ep = new IPEndPoint(tempAddress, 0);

Internals.SocketAddress socketAddress = IPEndPointExtensions.Serialize(ep);
errorCode = SocketPal.GetSockName(_handle, socketAddress.Buffer, ref socketAddress.InternalSize);
if (errorCode == SocketError.Success)
{
_rightEndPoint = ep.Create(socketAddress);
}

if (NetEventSource.IsEnabled) NetEventSource.Exit(this);
}

public SocketInformation DuplicateAndClose(int targetProcessId)
{
if (NetEventSource.IsEnabled) NetEventSource.Enter(this, targetProcessId);

ThrowIfDisposed();

SocketError errorCode = SocketPal.DuplicateSocket(_handle, targetProcessId, out SocketInformation info);

if (errorCode != SocketError.Success)
{
throw new SocketException((int)errorCode);
}

info.SetOption(SocketInformationOptions.Connected, Connected);
info.SetOption(SocketInformationOptions.NonBlocking, !Blocking);
info.SetOption(SocketInformationOptions.Listening, _isListening);

Close(timeout: -1);

if (NetEventSource.IsEnabled) NetEventSource.Exit(this);
return info;
}

private void EnsureDynamicWinsockMethods()
{
if (_dynamicWinsockMethods == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,6 @@ public Socket(AddressFamily addressFamily, SocketType socketType, ProtocolType p
if (NetEventSource.IsEnabled) NetEventSource.Exit(this);
}

public Socket(SocketInformation socketInformation)
{
//
// This constructor works in conjunction with DuplicateAndClose, which is not supported.
// See comments in DuplicateAndClose.
//
throw new PlatformNotSupportedException(SR.net_sockets_duplicateandclose_notsupported);
}

// Called by the class to create a socket to accept an incoming request.
private Socket(SafeSocketHandle fd)
{
Expand Down Expand Up @@ -2043,16 +2034,7 @@ private bool CanUseConnectEx(EndPoint remoteEP)
(_rightEndPoint != null || remoteEP.GetType() == typeof(IPEndPoint));
}

public SocketInformation DuplicateAndClose(int targetProcessId)
{
//
// On Windows, we cannot duplicate a socket that is bound to an IOCP. In this implementation, we *only*
// support IOCPs, so this will not work.
//
// On Unix, duplication of a socket into an arbitrary process is not supported at all.
//
throw new PlatformNotSupportedException(SR.net_sockets_duplicateandclose_notsupported);
}


internal IAsyncResult UnsafeBeginConnect(EndPoint remoteEP, AsyncCallback callback, object state, bool flowContext = false)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,16 @@ public struct SocketInformation
{
public byte[] ProtocolInformation { get; set; }
public SocketInformationOptions Options { get; set; }

internal void SetOption(SocketInformationOptions option, bool value)
{
if (value) Options |= option;
else Options &= ~option;
}

internal bool GetOption(SocketInformationOptions option)
{
return (Options & option) == option;
}
}
}
Loading