C# - 리눅스 환경에서의 Hyper-V Socket 연동 (AF_VSOCK)
(이 글의 후반에서 기록으로 남겼는데) 리눅스 환경의 닷넷 런타임 내부 코드는 Socket의 Address Family 자체에 대해 AF_VSOCK을 지원하지 않습니다. 이로 인해 C#으로 리눅스 환경의 vsock 통신을 해야 한다면 어쩔 수 없이 C++에 기반한 P/Invoke를 사용해야 합니다.
C++ 코드로는 지난 글에서 알아봤기 때문에,
C++ - Vsock 예제 (Hyper-V Socket 연동)
; https://www.sysnet.pe.kr/2/0/13666
그에 준하는 Interop 코드만 맞춰준 다음,
// CentOS Stream 9
// sudo dnf install dotnet-sdk-8.0
internal class Vsock
{
public static int AF_VSOCK => 40;
public const int VMADDR_PORT_ANY = -1;
public const int VMADDR_CID_ANY = -1;
public const int VMADDR_CID_HYPERVISOR = 0;
public const int VMADDR_CID_LOCAL = 1;
public const int VMADDR_CID_HOST = 2;
[DllImport("libc", SetLastError = true)]
internal static extern int socket(int af, int type, int protocol);
[DllImport("libc", SetLastError = true)]
internal static unsafe extern int bind(int s, SOCKADDR_VM* socketAddrName, int nameLen);
[DllImport("libc", SetLastError = true)]
internal static unsafe extern int listen(int s, int n);
[DllImport("libc", SetLastError = true)]
internal static unsafe extern int accept(int s);
[DllImport("libc", SetLastError = true)]
internal static unsafe extern int connect(int s, SOCKADDR_VM* socketAddrName, int nameLen);
[DllImport("libc", SetLastError = true)]
internal static extern int recv(int s, byte[] buffer, int size, int flags);
[DllImport("libc", SetLastError = true)]
internal static extern int send(int s, byte[] buffer, int size, int flags);
[DllImport("libc", SetLastError = true)]
internal static extern int close(int s);
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public unsafe struct SOCKADDR_VM
{
public static int StructSize => Marshal.SizeOf<SOCKADDR_VM>();
public ushort Family;
public ushort Reserved;
public uint SVM_Port;
public uint SVM_Cid;
public byte SVM_Flags;
//[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public fixed byte SVM_Zero[3];
}
전체적인 코드 구성은 C++에서 작성한 것처럼 하면 됩니다.
using ConsoleApp1;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
namespace ConsoleApp2;
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine($"Process ID: {Environment.ProcessId}");
if (args.Length == 0)
{
args = new[] { "/server", "any" };
}
uint cid = 1;
uint port = 19000;
if (args.Length >= 3)
{
uint.TryParse(args[2], out port);
}
if (args.Length >= 1)
{
if (args.Length >= 2)
{
if (uint.TryParse(args[1], out cid) == false)
{
switch (args[1])
{
case "any":
case "wildcard":
cid = unchecked((uint)-1);
break;
case "loopback":
case "lo":
cid = 1;
break;
case "parent":
case "host":
cid = 2;
break;
}
}
}
switch (args[0])
{
case "/server":
RunAsServer(cid, port);
return;
case "/client":
RunAsClient(cid, port);
return;
}
}
Console.WriteLine($"Usage: {nameof(ConsoleApp1)} /register");
Console.WriteLine($"Usage: {nameof(ConsoleApp1)} [/server | (empty)]");
Console.WriteLine($"Usage: {nameof(ConsoleApp1)} /client");
}
private static unsafe void RunAsClient(uint cid, uint port)
{
if (OperatingSystem.IsLinux() == false)
{
throw new NotSupportedException();
}
int client = Vsock.socket(Vsock.AF_VSOCK, (int)SocketType.Stream, 0);
if (client < 0)
{
int lastError = Marshal.GetLastWin32Error();
Console.WriteLine($"socket - {lastError}");
return;
}
SOCKADDR_VM sockAddr = new SOCKADDR_VM
{
Family = (ushort)Vsock.AF_VSOCK,
Reserved = 0,
SVM_Port = port,
SVM_Cid = cid,
};
int connected = Vsock.connect(client, &sockAddr, SOCKADDR_VM.StructSize);
Console.WriteLine($"socket = {client}");
byte[] buffer = Encoding.UTF8.GetBytes("World!");
Vsock.send(client, buffer, buffer.Length, 0);
buffer = new byte[1024];
int recvBytes = Vsock.recv(client, buffer, buffer.Length, 0);
string text = Encoding.UTF8.GetString(buffer, 0, recvBytes);
Console.WriteLine($"{text}");
Vsock.close(client);
}
private static unsafe void RunAsServer(uint cid, uint port)
{
if (OperatingSystem.IsLinux() == false)
{
throw new NotSupportedException();
}
int server = Vsock.socket(Vsock.AF_VSOCK, (int)SocketType.Stream, 0);
if (server < 0)
{
int lastError = Marshal.GetLastWin32Error();
Console.WriteLine($"socket - {lastError}");
return;
}
SOCKADDR_VM sockAddr = new SOCKADDR_VM
{
Family = (ushort)Vsock.AF_VSOCK,
Reserved = 0,
SVM_Port = port,
SVM_Cid = cid,
};
int result = Vsock.bind(server, &sockAddr, SOCKADDR_VM.StructSize);
if (result == -1)
{
int lastError = Marshal.GetLastWin32Error();
Console.WriteLine($"bind - {lastError}");
return;
}
Vsock.listen(server, 5);
ServerLoop(server);
Vsock.close(server);
}
private static void ServerLoop(int server_sock)
{
while (true)
{
int client_socket = Vsock.accept(server_sock);
if (client_socket < 0)
{
Console.WriteLine("accept failed!\n");
break;
}
Console.WriteLine($"connected: {client_socket}");
byte[] buffer = new byte[1024];
int recvBytes = Vsock.recv(client_socket, buffer, 1024, 0);
if (recvBytes != 0)
{
string text = $"Hello: {Encoding.UTF8.GetString(buffer, 0, recvBytes)}";
byte [] response = Encoding.UTF8.GetBytes(text);
Vsock.send(client_socket, response, response.Length, 0);
}
Vsock.close(client_socket);
}
}
}
(아직은) .NET Socket 타입을 사용할 수 없어 약간 불편할 수 있지만, 뭐 그런대로 C++의 경험을 그대로 살려 코딩하면 되겠습니다. ^^
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
현재 리눅스 환경에서 AF_VSOCK을 사용하면 "Address family not supported by protocol" 오류가 발생합니다.
server = new Socket((AddressFamily)40, SocketType.Stream, (ProtocolType)0);
/*
System.Net.Sockets.SocketException
HResult=0x80004005
Message=Address family not supported by protocol
Source=System.Net.Sockets
StackTrace:
at System.Net.Sockets.Socket..ctor(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType)
...[생략]...
*/
윈도우 버전의 경우
EndPoint를 재정의하면 되었는데, 왜 리눅스는 그게 안 되는 것일까요? 잠시 소스코드를 추적해 보면, 내부적으로 TryConvertAddressFamilyPalToPlatform 함수가 불리는데,
// https://github.com/dotnet/runtime/blob/7294674995d2441075b5b4ad151b6e8982ee9c6f/src/native/libs/System.Native/pal_networking.c#L214C13
static bool TryConvertAddressFamilyPalToPlatform(int32_t palAddressFamily, sa_family_t* platformAddressFamily)
{
assert(platformAddressFamily != NULL);
switch (palAddressFamily)
{
case AddressFamily_AF_UNSPEC:
*platformAddressFamily = AF_UNSPEC;
return true;
case AddressFamily_AF_UNIX:
*platformAddressFamily = AF_UNIX;
return true;
case AddressFamily_AF_INET:
*platformAddressFamily = AF_INET;
return true;
case AddressFamily_AF_INET6:
*platformAddressFamily = AF_INET6;
return true;
#ifdef AF_PACKET
case AddressFamily_AF_PACKET:
*platformAddressFamily = AF_PACKET;
return true;
#endif
#ifdef AF_CAN
case AddressFamily_AF_CAN:
*platformAddressFamily = AF_CAN;
return true;
#endif
default:
*platformAddressFamily = (sa_family_t)palAddressFamily;
return false;
}
}
(나중에 나오지만, Linux에서는 저런 식으로 지원 가능한 목록이 정해져 있고, 윈도우는 단순히 ushort.MaxValue 내의 값이면 받아들이게 코딩이 돼 있습니다.)
*platformAddressFamily에 값은 보관되지만 false를 리턴하는 바람에,
// https://github.com/dotnet/runtime/blob/cffaa78235ea93d5e3eeb56956579df503e11250/src/libraries/Native/Unix/System.Native/pal_networking.c#L2461
int32_t SystemNative_Socket(int32_t addressFamily, int32_t socketType, int32_t protocolType, intptr_t* createdSocket)
{
if (createdSocket == NULL)
{
return Error_EFAULT;
}
sa_family_t platformAddressFamily;
int platformSocketType, platformProtocolType;
if (!TryConvertAddressFamilyPalToPlatform(addressFamily, &platformAddressFamily))
{
*createdSocket = -1;
return Error_EAFNOSUPPORT;
}
// ...[생략]...
}
결국 Error_EAFNOSUPPORT 오류 코드를 반환하기 때문입니다.
이를 우회해서, SafeSocketHandle을 이용해 socket 함수를 직접 호출하는 방법도 있습니다.
[DllImport("libc", SetLastError = true)]
public static extern int socket(int domain, int type, int protocol);
int fd = HyperVSocket.socket((int)HyperVSocket.AF_VSOCK, (int)SocketType.Stream, (int)ProtocolType.Unspecified);
Socket server = new Socket(new SafeSocketHandle((IntPtr)fd, true));
server.Bind(new HVEndPoint(serviceGuid, vmId));
하지만, 그래도 결국 HVEndPoint에서 SocketAddress를 생성하며 오류가 발생합니다.
public unsafe override SocketAddress Serialize()
{
SocketAddress sa;
if (OperatingSystem.IsLinux())
{
sa = new SocketAddress(HyperVSocket.AF_VSOCK); // Address family not supported by protocol
...[생략]...
}
return sa;
}
왜냐하면 SocketAddress 생성자에서도 address family에 대해, 결국 TryConvertAddressFamilyPalToPlatform 함수로 흘러가면서 예외가 발생합니다.
public SocketAddress(AddressFamily family, int size)
{
ArgumentOutOfRangeException.ThrowIfLessThan(size, MinSize);
_size = size;
_buffer = new byte[size];
_buffer[0] = (byte)_size;
SocketAddressPal.SetAddressFamily(_buffer, family);
}
// https://github.com/dotnet/runtime/blob/7294674995d2441075b5b4ad151b6e8982ee9c6f/src/libraries/Common/src/System/Net/SocketAddressPal.Unix.cs#L73
public static unsafe void SetAddressFamily(Span<byte> buffer, AddressFamily family)
{
Interop.Error err;
if (family != AddressFamily.Unknown)
{
fixed (byte* rawAddress = buffer)
{
err = Interop.Sys.SetAddressFamily(rawAddress, buffer.Length, (int)family);
}
ThrowOnFailure(err);
}
}
// https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SocketAddress.cs#L18
internal static partial class Interop
{
internal static partial class Sys
{
// ...[생략]...
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetAddressFamily")]
[SuppressGCTransition]
internal static unsafe partial Error GetAddressFamily(byte* socketAddress, int socketAddressLen, int* addressFamily);
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_SetAddressFamily")]
[SuppressGCTransition]
internal static unsafe partial Error SetAddressFamily(byte* socketAddress, int socketAddressLen, int addressFamily);
// ...[생략]...
}
}
// https://github.com/dotnet/runtime/blob/cffaa78235ea93d5e3eeb56956579df503e11250/src/libraries/Native/Unix/System.Native/pal_networking.c#L695C1
int32_t SystemNative_SetAddressFamily(uint8_t* socketAddress, int32_t socketAddressLen, int32_t addressFamily)
{
struct sockaddr* sockAddr = (struct sockaddr*)socketAddress;
if (sockAddr == NULL || socketAddressLen < 0 ||
!IsInBounds(sockAddr, (size_t)socketAddressLen, &sockAddr->sa_family, sizeof_member(sockaddr, sa_family)))
{
return Error_EFAULT;
}
if (!TryConvertAddressFamilyPalToPlatform(addressFamily, &sockAddr->sa_family))
{
return Error_EAFNOSUPPORT;
}
return Error_SUCCESS;
}
// https://github.com/dotnet/runtime/blob/cffaa78235ea93d5e3eeb56956579df503e11250/src/libraries/Native/Unix/System.Native/pal_networking.c#L674
int32_t SystemNative_GetAddressFamily(const uint8_t* socketAddress, int32_t socketAddressLen, int32_t* addressFamily)
{
if (socketAddress == NULL || addressFamily == NULL || socketAddressLen < 0)
{
return Error_EFAULT;
}
const struct sockaddr* sockAddr = (const struct sockaddr*)socketAddress;
if (!IsInBounds(sockAddr, (size_t)socketAddressLen, &sockAddr->sa_family, sizeof_member(sockaddr, sa_family)))
{
return Error_EFAULT;
}
if (!TryConvertAddressFamilyPlatformToPal(sockAddr->sa_family, addressFamily))
{
*addressFamily = AddressFamily_AF_UNKNOWN;
}
return Error_SUCCESS;
}
위의 소스코드에서 SetAddressFamily의 구현을 보면 굳이 (내부적으로 TryConvertAddressFamilyPalToPlatform을 호출하는) Interop.Sys.SetAddressFamily 메서드를 호출해 확인하고 있는데요, 이 부분을 윈도우에서는 C# 소스코드 내에서 단순히 다음과 같이 처리하기 때문에,
// https://github.com/dotnet/runtime/blob/7294674995d2441075b5b4ad151b6e8982ee9c6f/src/libraries/Common/src/System/Net/SocketAddressPal.Windows.cs#L21
public static void SetAddressFamily(Span buffer, AddressFamily family)
{
if ((int)(family) > ushort.MaxValue)
{
// For legacy values family maps directly to Winsock value.
// Other values will need mapping if/when supported.
// Currently, that is Netlink, Packet and ControllerAreaNetwork, neither of them supported on Windows.
throw new PlatformNotSupportedException();
}
#if BIGENDIAN
BinaryPrimitives.WriteUInt16BigEndian(buffer, (ushort)family);
#else
BinaryPrimitives.WriteUInt16LittleEndian(buffer, (ushort)family);
#endif}
}
EndPoint를 재정의하는 것이 가능했던 것입니다.
이와 관련한 이슈가 2021년부터 Open 상태인 것을 보면,
Cannot create AF_VSOCK Sockets due to overly strict SocketPal #58327
; https://github.com/dotnet/runtime/issues/58327
해결될 기미가 보이지 않습니다. 하지만, 위의 소스코드 추적에서 봤던 것처럼 단순히 ushort.MaxValue로 넓혀주면 해결될 일이기 때문에 아마도 이슈 자체는 언제든 쉽게 완료되지 않을까 싶습니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]