dotnet / dotnet/runtime

Migrate Socket between processes (RE: #48637)

Open
#121,859 4 comments 0 reactions 0 assignees View on GitHub
area-System.Net.Sockets
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

### Description

In continuation to #48637, I have successfully gotten this to work.

I have reason to believe there needs be some investigation with the Interop for Socket.Pal.Duplicate().
The WSAProtocol_InfoW does not seem to be fully supported to replicate the needed properties to make this work properly.
Using Reflection, I can poke at the required fields/properties to fix this after IPC is performed.

Few things to consider:
1. The SocketInformation on serialization is not fully symmetric.
a. RemoteEndpoint - Will be null on Socket.Pal.Duplicate()
b. Inability to serialize a 'RemoteEndpoint' object using System.Json.Serializer will need to convert to a byte[] or rebuild it on the remote endpoint after IPC is performed.
2. Wrap SocketInformation2 to preserve 1. in an easy fashion.

### Reproduction Steps

```
internal class SocketInformation2
{
public SocketInformation SI { get; set; }
public int Port { get; set; }
public string? Address { get; set; }
}

internal static class Extensions
{
private static unsafe SocketError DuplicateSocket(SafeSocketHandle handle, int targetProcessId, out SocketInformation2 socketInformation2)
{
var socketInformation = new SocketInformation
{
#pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type
ProtocolInformation = new byte[sizeof(Interop.Winsock.WSAPROTOCOL_INFOW)]
#pragma warning restore CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type
};

socketInformation2 = new SocketInformation2
{
SI = socketInformation
};

fixed (byte* protocolInfoBytes = socketInformation.ProtocolInformation)
{
var lpProtocolInfo = (Interop.Winsock.WSAPROTOCOL_INFOW*)protocolInfoBytes;
var result = Interop.Winsock.WSADuplicateSocket(handle, (uint)targetProcessId, lpProtocolInfo);
return result == 0 ? SocketError.Success : GetLastSocketError();
}
}

[SupportedOSPlatform("windows")]
public static SocketInformation2 Duplicate(this Socket socket, int targetProcessId)
{
var errorCode = DuplicateSocket(socket.SafeHandle, targetProcessId, out var info);

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

info.SI = info.SI with
{
Options = SocketInformationOptions.Connected | SocketInformationOptions.Listening
};

//Close(timeout: -1);

return info;
}

private static SocketError GetLastSocketError()
{
var win32Error = Marshal.GetLastPInvokeError();
Debug.Assert(win32Error != 0, "Expected non-0 error");
return (SocketError)win32Error;
}
}

// The rest of this was ripped out of the GitHub open source....
///////////////////////////////////////////////////////////////////////////

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

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

internal static partial class Interop
{
internal static partial class Winsock
{
[LibraryImport("Ws2_32.dll", EntryPoint = "WSADuplicateSocketW", SetLastError = true)]
internal static unsafe partial int WSADuplicateSocket(
SafeSocketHandle s,
uint dwProcessId,
WSAPROTOCOL_INFOW* lpProtocolInfo
);
}
}

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

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

internal static partial class Interop
{
internal static partial class Winsock
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal unsafe struct WSAPROTOCOL_INFOW
{
private const int WSAPROTOCOL_LEN = 255;

internal uint dwServiceFlags1;
internal uint dwServiceFlags2;
internal uint dwServiceFlags3;
internal uint dwServiceFlags4;
internal uint dwProviderFlags;
internal Guid ProviderId;
internal uint dwCatalogEntryId;
internal WSAPROTOCOLCHAIN ProtocolChain;
internal int iVersion;
internal AddressFamily iAddressFamily;
internal int iMaxSockAddr;
internal int iMinSockAddr;
internal SocketType iSocketType;
internal ProtocolType iProtocol;
internal int iProtocolMaxOffset;
internal int iNetworkByteOrder;
internal int iSecurityScheme;
internal uint dwMessageSize;
internal uint dwProviderReserved;
internal fixed char szProtocol[WSAPROTOCOL_LEN + 1];
}

[StructLayout(LayoutKind.Sequential)]
internal unsafe struct WSAPROTOCOLCHAIN
{
private const int MAX_PROTOCOL_CHAIN = 7;

internal int ChainLen;
internal fixed uint ChainEntries[MAX_PROTOCOL_CHAIN];
}
}
}

```
### Expected behavior

1. Was expecting to have 'RemoteEndpoint' not be null.

### Actual behavior

Can hot-reboot process without interrupting remote socket endpoints.
Essentially, performs a net-split similar to IRC back in the day for Windows using the current/existing IOCP model.
Sent a message 'asdf' from the Listener after RE-Hup to the client... which was successfully received as expected.

### Regression?

![Image](https://github.com/user-attachments/assets/dd77f5e8-0f4f-4ae8-8621-85cb83684a58)

1. Initial connection to Parent process via TcpListener & TcpClient polling which I do asynchronously.
2. After connections are established.. send a 'Reboot' command to essentially respawn the process.
a. 'Stop' the listener
b. Serialize the 'SocketInformation2' via IPC using named pipes between the two processes.
3. After serialization is completed on the Parent process, you can immediately exit.
4. On the new process... deserialize 2. and rebuild your connection FD list. (See below)

### Known Workarounds

1. Had to wrap 'SocketInformation' as it was failing to set the 'RemoteEndpoint' properly.

```
const BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic;
var tcpClient = new TcpClient();
var socketInfo = JsonSerializer.Deserialize(json);
tcpClient.Client = new Socket(socketInfo!.SI);

while (!tcpClient.Client.Connected)
Task.Delay(TimeSpan.FromMilliseconds(500)).GetAwaiter();

var prop = tcpClient.Client.GetType().GetField("_remoteEndPoint", flags);
prop!.SetValue(tcpClient.Client, new IPEndPoint(IPAddress.Parse(socketInfo.Address!), socketInfo.Port), flags, null, null);

InitializeSocket(tcpClient);
```

### Configuration

Which version of .NET is the code running on? **.NET 10**
What OS and version, and what distro if applicable? **Windows 11 (OS Build 22631.6199)**
What is the architecture (x64, x86, ARM, ARM64)? **x64**
Do you know whether it is specific to that configuration? **?**
If you're using Blazor, which web browser(s) do you see this issue in? **WPF Server / Python Client**

### Other information

This answers the OPs question for #48637 and does not require a proxy or listener to pass along Remote information to its child pids'.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.