Microsoft MVP성태의 닷넷 이야기
닷넷: 2273. C# - 리눅스 환경에서의 Hyper-V Socket 연동 (AF_VSOCK) [링크 복사], [링크+제목 복사],
조회: 6720
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일

(시리즈 글이 6개 있습니다.)
닷넷: 2270. C# - Hyper-V Socket 통신(AF_HYPERV, AF_VSOCK)을 위한 EndPoint 사용자 정의
; https://www.sysnet.pe.kr/2/0/13657

닷넷: 2272. C# - Hyper-V Socket 통신(AF_HYPERV, AF_VSOCK)의 VMID Wildcards 유형
; https://www.sysnet.pe.kr/2/0/13663

Linux: 73. Linux 측의 socat을 이용한 Hyper-V 호스트와의 vsock 테스트
; https://www.sysnet.pe.kr/2/0/13665

Linux: 74. C++ - Vsock 예제 (Hyper-V Socket 연동)
; https://www.sysnet.pe.kr/2/0/13666

닷넷: 2273. C# - 리눅스 환경에서의 Hyper-V Socket 연동 (AF_VSOCK)
; https://www.sysnet.pe.kr/2/0/13667

개발 환경 구성: 721. WSL 2에서의 Hyper-V Socket 연동
; https://www.sysnet.pe.kr/2/0/13713




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로 넓혀주면 해결될 일이기 때문에 아마도 이슈 자체는 언제든 쉽게 완료되지 않을까 싶습니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 8/6/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 121  122  123  124  125  126  127  128  129  130  [131]  132  133  134  135  ...
NoWriterDateCnt.TitleFile(s)
1781정성태10/21/201420614디버깅 기술: 64. new/delete의 짝이 맞는 경우에도 메모리 누수가 발생한다면?
1780정성태10/15/201424258오류 유형: 249. The application-specific permission settings do not grant Local Activation permission for the COM Server application with CLSID
1779정성태10/15/201419773오류 유형: 248. Active Directory에서 OU가 지워지지 않는 경우
1778정성태10/10/201418242오류 유형: 247. The Netlogon service could not create server share C:\Windows\SYSVOL\sysvol\[도메인명]\SCRIPTS.
1777정성태10/10/201421347오류 유형: 246. The processing of Group Policy failed. Windows attempted to read the file \\[도메인]\sysvol\[도메인]\Policies\{...GUID...}\gpt.ini
1776정성태10/10/201418353오류 유형: 245. 이벤트 로그 - Name resolution for the name _ldap._tcp.dc._msdcs.[도메인명]. timed out after none of the configured DNS servers responded.
1775정성태10/9/201419472오류 유형: 244. Visual Studio 디버깅 (2) - Unable to break execution. This process is not currently executing the type of code that you selected to debug.
1774정성태10/9/201426662개발 환경 구성: 246. IIS 작업자 프로세스의 20분 자동 재생(Recycle)을 끄는 방법
1773정성태10/8/201429837.NET Framework: 471. 웹 브라우저로 다운로드가 되는 파일을 왜 C# 코드로 하면 안되는 걸까요? [1]
1772정성태10/3/201418620.NET Framework: 470. C# 3.0의 기본 인자(default parameter)가 .NET 1.1/2.0에서도 실행될까? [3]
1771정성태10/2/201428125개발 환경 구성: 245. 실행된 프로세스(EXE)의 명령행 인자를 확인하고 싶다면 - Sysmon [4]
1770정성태10/2/201421720개발 환경 구성: 244. 매크로 정의를 이용해 파일 하나로 C++과 C#에서 공유하는 방법 [1]파일 다운로드1
1769정성태10/1/201424144개발 환경 구성: 243. Scala 개발 환경 구성(JVM, 닷넷) [1]
1768정성태10/1/201419561개발 환경 구성: 242. 배치 파일에서 Thread.Sleep 효과를 주는 방법 [5]
1767정성태10/1/201424701VS.NET IDE: 94. Visual Studio 2012/2013에서의 매크로 구현 - Visual Commander [2]
1766정성태10/1/201422531개발 환경 구성: 241. 책 "프로그래밍 클로저: Lisp"을 읽고 나서. [1]
1765정성태9/30/201426076.NET Framework: 469. Unity3d에서 transform을 변수에 할당해 사용하는 특별한 이유가 있을까요?
1764정성태9/30/201422316오류 유형: 243. 파일 삭제가 안 되는 경우 - The action can't be comleted because the file is open in System
1763정성태9/30/201423884.NET Framework: 468. PDB 파일을 연동해 소스 코드 라인 정보를 알아내는 방법파일 다운로드1
1762정성태9/30/201424582.NET Framework: 467. 닷넷에서 EIP/RIP 레지스터 값을 구하는 방법 [1]파일 다운로드1
1761정성태9/29/201421654.NET Framework: 466. 윈도우 운영체제의 보안 그룹 이름 및 설명 문자열을 바꾸는 방법파일 다운로드1
1760정성태9/28/201419915.NET Framework: 465. ICorProfilerInfo::GetILToNativeMapping 메서드가 0x80131358을 반환하는 경우
1759정성태9/27/201431040개발 환경 구성: 240. Visual C++ / x64 환경에서 inline-assembly를 매크로 어셈블리로 대체하는 방법파일 다운로드1
1758정성태9/23/201437920개발 환경 구성: 239. 원격 데스크톱 접속(RDP)을 기존의 콘솔 모드처럼 사용하는 방법 [1]
1757정성태9/23/201418496오류 유형: 242. Lync로 모임 참여 시 소리만 들리지 않는 경우 - 두 번째 이야기
1756정성태9/23/201427486기타: 48. NVidia 제품의 과다한 디스크 사용 [2]
... 121  122  123  124  125  126  127  128  129  130  [131]  132  133  134  135  ...