Microsoft MVP성태의 닷넷 이야기
닷넷: 2273. C# - 리눅스 환경에서의 Hyper-V Socket 연동 (AF_VSOCK) [링크 복사], [링크+제목 복사],
조회: 6736
글쓴 사람
정성태 (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)
10874정성태11/26/201526404개발 환경 구성: 278. 인증서와 인증서를 이용한 코드 사인의 해시 구분
10873정성태11/25/201525498.NET Framework: 540. C# - 부동 소수 계산 왜 이렇게 나오죠? (2) [3]파일 다운로드1
10872정성태11/24/201532527.NET Framework: 539. C# - 부동 소수 계산 왜 이렇게 나오죠? (1) [1]
10871정성태11/23/201528126오류 유형: 313. SignTool Error: No certificates were found that met all the given criteria.
10870정성태11/23/201528988오류 유형: 312. 윈도우 10 TH2 (버전 1511) 업데이트가 안되는 경우 [1]
10869정성태11/23/201524777오류 유형: 311. certutil 실행 오류 - 0x80070057 [1]
10868정성태11/20/201524639제니퍼 .NET: 25. 제니퍼 닷넷 적용 사례 (5) - RestSharp 라이브러리의 CPU High 현상파일 다운로드1
10867정성태10/18/201527199.NET Framework: 538. Thread.Abort로 인해 프로세스가 종료되는 현상
10866정성태10/14/201523396.NET Framework: 537. C# - Reflection의 박싱 없이 값 형식을 다루는 방법파일 다운로드1
10865정성태10/13/201523166.NET Framework: 536. Thread.Abort의 스레드 종료 지연파일 다운로드1
10864정성태10/12/201521411.NET Framework: 535. aspnet.config 파일의 설정을 읽는 방법
10863정성태10/9/201526206.NET Framework: 534. ASP.NET 응용 프로그램이 예외로 프로세스가 종료된다면?
10862정성태10/9/201524511오류 유형: 310. 비주얼 스튜디오 - Unspecified error (Exception from HRESULT: 0x80004005 (E_FAIL))
10861정성태10/9/201529201기타: 54. 도서: 시작하세요! C# 6.0 프로그래밍: 기본 문법부터 실전 예제까지 (2)
10860정성태10/5/201526644개발 환경 구성: 277. IIS AppPool의 시작/중단에 대한 이벤트 로그 확인 방법
10859정성태10/5/201527825.NET Framework: 533. C#에서 string 형식이 primitive일까요? [6]
10858정성태10/2/201524368VS.NET IDE: 105. Visual Studio의 단위 테스트 작성 시 Fakes를 이용한 메서드 재정의 방법 [1]파일 다운로드1
10857정성태10/1/201520356VS.NET IDE: 104. Visual C++ 프로젝트의 빌드 이벤트에서 환경 변수 사용하는 방법
10856정성태9/30/201531687.NET Framework: 532. WPF DataGrid의 데이터 바인딩 시 리플렉션의 부하는 어느 정도일까요?파일 다운로드1
10855정성태9/30/201521309.NET Framework: 531. C# - XSLT 내의 javascript에 전달한 XML 노드의 타입은?
10854정성태9/30/201521841오류 유형: 309. C# - 포인터를 쓰는 경우 VerificationException이 발생한다면?
10853정성태9/21/201519584오류 유형: 308. 공백 문자를 포함한 계정명의 권한으로 Visual Studio 확장을 설치할 때 오류 발생
10852정성태9/17/201524634VC++: 92. C++ 생성자를 DLL로부터 동적 로드해 객체를 생성한다면? [2]파일 다운로드1
10851정성태9/15/201524328.NET Framework: 530. C# - 중위식을 후위식으로 변환하는 예제파일 다운로드1
10850정성태9/14/201522968.NET Framework: 529. C# - volatile 키워드로 인한 차이점을 발생시키는 예제 [1]파일 다운로드1
10849정성태9/14/201557103오류 유형: 307. CLR20r3 오류 해결을 위해 mscorlib.dll을 덮어쓸때 주의할 점 [12]
... 121  [122]  123  124  125  126  127  128  129  130  131  132  133  134  135  ...