Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (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_HYPERV, AF_VSOCK)의 VMID Wildcards 유형

이전에 설명한 Hyper-V Socket 통신은,

C# - Hyper-V Socket 통신(AF_HYPERV, AF_VSOCK)을 위한 EndPoint 사용자 정의
; https://www.sysnet.pe.kr/2/0/13657

서버와 클라이언트 간의 다양한 응용이 가능하도록 미리 지정된 HV GUID 값을 지원합니다.

HV_GUID_ZERO	    00000000-0000-0000-0000-000000000000    Listeners should bind to this VmId to accept connection from all partitions.

HV_GUID_WILDCARD    00000000-0000-0000-0000-000000000000    Listeners should bind to this VmId to accept connection from all partitions.

HV_GUID_BROADCAST   FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF	

HV_GUID_CHILDREN    90db8b89-0d35-4f79-8ce9-49ea0ac8b7cd    Wildcard address for children. Listeners should bind to this VmId to accept connection from its children.

HV_GUID_LOOPBACK    e0e16197-dd56-4a10-9195-5ee7a155a838    Loopback address. Using this VmId connects to the same partition as the connector.

HV_GUID_PARENT	    a42e7cda-d03f-480c-9cc2-a4de20abb878    Parent address. Using this VmId connects to the parent partition of the connector.*

사실 이름만으로도 역할들을 대충 눈치챌 수 있지만, 그래도 정말 그런지 테스트를 해보고 싶습니다. ^^ 이를 위해 지난 글에 설명한 코드를 다음과 같이 확장해서 만들어 봤습니다.

using Microsoft.Win32;
using System.Net.Sockets;
using System.Text;

namespace ConsoleApp1;

internal class Program
{
    // 윈도우 VM과만 연동하는 경우
    // public static readonly Guid GcsGuid = new Guid("9866C7BC-0C89-4CB0-9427-5B526D1AE83B");
    // 리눅스 VM도 함께 연동하는 경우,
    public static readonly Guid GcsGuid = new Guid("00004A38-facb-11e6-bd58-64006a7986d3");

    public const string GcsName = "MyGcs";

    static void Main(string[] args)
    {
        Console.WriteLine($"Process ID: {Environment.ProcessId}");

        if (args.Length == 0)
        {
            args = new[] { "/server", "lo" };
        }

        Guid serviceGuid = GcsGuid;
        if (args.Length >= 3)
        {
            if (Guid.TryParse(args[2], out Guid guid))
            {
                serviceGuid = guid;
            }
        }

        if (args.Length >= 1)
        {
            Guid vmid = HyperVSocket.HV_GUID_LOOPBACK;
            if (args.Length >= 2)
            {
                if (Guid.TryParse(args[1], out Guid guid))
                {
                    vmid = guid;
                }
                else
                {
                    switch (args[1])
                    {
                        case "any":
                        case "wildcard":
                            vmid = HyperVSocket.HV_GUID_WILDCARD;
                            break;

                        case "loopback":
                        case "lo":
                            vmid = HyperVSocket.HV_GUID_LOOPBACK;
                            break;

                        case "child":
                        case "children":
                            vmid = HyperVSocket.HV_GUID_CHILDREN;
                            break;

                        case "parent":
                        case "host":
                            vmid = HyperVSocket.HV_GUID_PARENT;
                            break;
                    }
                }
            }

            switch (args[0])
            {
                case "/register":
                    Register();
                    return;

                case "/server":
                    RunAsServer(serviceGuid, vmid);
                    return;

                case "/client":
                    RunAsClient(serviceGuid, vmid);
                    return;
            }
        }

        Console.WriteLine($"Usage: {nameof(ConsoleApp1)} /register");
        Console.WriteLine($"Usage: {nameof(ConsoleApp1)} [/server | (empty)] [lo|any]");
        Console.WriteLine($"Usage: {nameof(ConsoleApp1)} /client [lo|vmid|parent]");
    }

    private static void RunAsClient(Guid serviceGuid, Guid vmId)
    {
        Socket client;

        if (OperatingSystem.IsLinux())
        {
            throw new NotSupportedException();
        }
        else
        {
            using (client = new Socket(HyperVSocket.AF_HYPERV, SocketType.Stream, HyperVSocket.HV_PROTOCOL_RAW))
            {
                client.Connect(new HVEndPoint(serviceGuid, vmId));

                Console.WriteLine($"Connected to {serviceGuid}, {vmId}");

                byte[] buffer = Encoding.UTF8.GetBytes("World!");
                client.Send(buffer);

                buffer = new byte[1024];
                int recvBytes = client.Receive(buffer);
                string response = Encoding.UTF8.GetString(buffer, 0, recvBytes);
                Console.WriteLine(response);
            }
        }
    }

    private static void RunAsServer(Guid serviceGuid, Guid vmId)
    {
        Socket server;

        if (OperatingSystem.IsLinux())
        {
            throw new NotSupportedException();
        }
        else
        {
            server = new Socket(HyperVSocket.AF_HYPERV, SocketType.Stream, HyperVSocket.HV_PROTOCOL_RAW);
        }

        server.Bind(new HVEndPoint(serviceGuid, vmId));
        server.Listen(5);

        ServerLoop(server);
    }

    private static void ServerLoop(Socket server)
    {
        while (true)
        {
            Socket socket = server.Accept();

            Console.WriteLine($"connected: {socket.RemoteEndPoint}");

            byte[] buffer = new byte[1024];
            int recvBytes = socket.Receive(buffer);

            string text = Encoding.UTF8.GetString(buffer, 0, recvBytes);
            Console.WriteLine(text);

            byte[] response = Encoding.UTF8.GetBytes($"Hello: {text}");
            socket.Send(response);
            socket.Close();
        }
    }

    private static void Register()
    {
        Console.WriteLine("Registering...");

        if (!OperatingSystem.IsWindows())
        {
            Console.WriteLine("This feature is only supported on Windows.");
            return;
        }

        RegistryKey? gcsKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization\GuestCommunicationServices", true);
        var subKey = gcsKey?.CreateSubKey($"{GcsGuid:D}", true);
        subKey?.SetValue("ElementName", GcsName);

        Console.WriteLine($"Registered: {GcsGuid}");
    }
}

테스트 방법은, 우선 Hyper-V를 호스팅하는 윈도우 측에 서비스를 등록한 다음,

// 관리자 권한으로 실행

C:\temp> ConsoleApp1.exe /register

기본적으로 /server, /client 옵션을 통해 역할을 지정하면서 두 번째 인자로 HV_* GUID 모드를 지정할 수 있습니다. 이에 기반해 다양한 실험을 해볼까요? ^^





1. 소켓 서버에서 HV_GUID_ZERO, HV_GUID_WILDCARD 바인딩

HV_GUID_ZERO, HV_GUID_WILDCARD 2개는 모두 Guid.Empty로 같은 값인데요, 소켓 서버 측에서 이 값을 지정해 바인딩하면,

server = new Socket(HyperVSocket.AF_HYPERV, SocketType.Stream, HyperVSocket.HV_PROTOCOL_RAW);
server.Bind(new HVEndPoint(GcsGuid, HyperVSocket.HV_GUID_WILDCARD));

저 소켓 서버를 응용한 연결은 다음과 같은 유형으로 가능합니다.

[Host에서 서버를 실행하는 경우]
c:\temp> ConsoleApp1.exe /server wildcard

    * (O) VM에서 클라이언트를 실행해 연결 가능 (HV_GUID_PARENT)
        c:\temp> ConsoleApp1.exe /client parent
    * (O) 같은 호스트에서도 클라이언트를 실행해 연결 가능 (HV_GUID_LOOPBACK)
        c:\temp> ConsoleApp1.exe /client loopback

[VM에서 서버를 실행하는 경우]
c:\temp> ConsoleApp1.exe /server wildcard

    * (O) 호스트에서 클라이언트 실행해 연결 가능 (HV_GUID_LOOPBACK)
        c:\temp> ConsoleApp1.exe /client loopback
    * (O) VM 내에서 클라이언트 실행해 연결 가능 (이때 VM ID가 B50F20E6-CF19-1796-BA60-D9CFE2D8FE29인 것으로 가정)
        c:\temp> ConsoleApp1.exe /client B50F20E6-CF19-1796-BA60-D9CFE2D8FE29

그러니까, HV_GUID_WILDCARD는 마치 TCP 소켓의 IPAddress.Any와 같은 역할을 하는 것입니다.


2. 소켓 서버에서 HV_GUID_LOOPBACK 바인딩

그 이름에 따라 HV_GUID_LOOPBACK 바인딩은 IPAddress.Loopback을 연상시키는데요, 따라서 원래는 같은 VM 또는 Host 내에서만 연결이 가능해야 할 거라고 생각할 수 있지만, 실제로 해보면 이런 결과가 나옵니다.

[Host에서 서버를 실행하는 경우]
c:\temp> ConsoleApp1.exe /server loopback

    * (O) VM에서 클라이언트 실행해 연결 가능 (HV_GUID_PARENT)
        c:\temp> ConsoleApp1.exe /client parent
    * (O) 같은 호스트에서도 클라이언트 실행해 연결 가능 (HV_GUID_LOOPBACK)
        c:\temp> ConsoleApp1.exe /client loopback

[VM에서 서버를 실행하는 경우]
c:\temp> ConsoleApp1.exe /server loopback

    * (X) 호스트에서 클라이언트 실행해 연결 실패 (이때 VM의 ID가 B50F20E6-CF19-1796-BA60-D9CFE2D8FE29인 것으로 가정)
        c:\temp> ConsoleApp1.exe /client B50F20E6-CF19-1796-BA60-D9CFE2D8FE29
    * (O) VM 내에서 클라이언트 실행해 연결 가능 (HV_GUID_LOOPBACK)
        c:\temp> ConsoleApp1.exe /client loopback

그러니까, 특이하게도 Host에서 서버를 HV_GUID_LOOPBACK으로 바인딩해도, VM 내의 클라이언트는 HV_GUID_PARENT를 이용해 자유롭게 접근이 가능합니다. 반면, VM에서 서버를 실행하면 일반적인 소켓의 Loopback처럼 VM 내에서만 접근이 가능합니다.

참고로, 같은 호스트 및 VM 내에서도 연결이 된다는 점에서, 어쩌면 파일 관리를 해야 하는 Unix Domain 소켓보다 더 편리한데요, 여차하면 이걸로 그냥 대체하는 것이 더 나을 듯합니다. ^^


3. 소켓 서버에서 HV_GUID_CHILDREN 바인딩

HV_GUID_CHILDREN도 일단은 위의 2번 결과(HV_GUID_LOOPBACK)와 같습니다.

[Host에서 서버를 실행하는 경우]
c:\temp> ConsoleApp1.exe /server children

    * (O) VM에서 클라이언트 실행해 연결 가능 (HV_GUID_PARENT)
        c:\temp> ConsoleApp1.exe /client parent
    * (O) 같은 호스트에서도 클라이언트 실행해 연결 가능 (HV_GUID_LOOPBACK)
        c:\temp> ConsoleApp1.exe /client loopback

[VM에서 서버를 실행하는 경우]
c:\temp> ConsoleApp1.exe /server children

    * (X) 호스트에서 클라이언트 실행해 연결 실패 (이때 VM의 ID가 B50F20E6-CF19-1796-BA60-D9CFE2D8FE29인 것으로 가정)
        c:\temp> ConsoleApp1.exe /client B50F20E6-CF19-1796-BA60-D9CFE2D8FE29
    * (O) VM 내에서 클라이언트 실행해 연결 가능 (HV_GUID_LOOPBACK)
        c:\temp> ConsoleApp1.exe /client loopback

음... 어떻게 테스트를 해야 다른 점을 부각시킬 수 있을지 잘 모르겠군요. ^^; (혹시 아시는 분은 덧글 부탁드립니다.)

그래도 이 정도면 Hyper-V Socket에 대한 이해를 어느 정도 하셨으리라 생각합니다. ^^

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




부가적으로 아래의 소스 코드를 보면,

HyperVSockets/Program.cs
; https://github.com/Wraith2/HyperVSockets/blob/master/Program.cs

"// are we running as a hyper-v guest with integration services enabled?" 이런 주석과 함께 레지스트리 키 값을 하나 조사하고 있습니다.

경로: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Virtual Machine\Guest\Parameters
이름: VirtualMachineId
타입 REG_SZ
값: (...vm-id...)

VM 내에서 저 레지스트리 값이 있다면 해당 VM은 Hyper-V 호스트 측의 Integration Services가 활성화된 상태라고 판단하는 것 같습니다. 그 외에도, VM 스스로 자신의 VM ID를 알아낼 레지스트리 값이 있다는 정도만 기억하셔도 좋겠습니다.




당연한 이야기지만 Hyper-V 소켓은 TCP 통신이 아니므로 (TCP/IP network connections 정보를 열거하는) netstat로는 연결 정보를 알 수 없습니다. (혹시 열거 방법을 아시는 분은 덧글 부탁드립니다. ^^) (2024-07-06 업데이트: ETW를 이용한 추적이 가능한 것 같습니다.)

대신, 리눅스에서는 ss를 이용해 vsock 연결 정보를 확인할 수 있는데요, 가령 socat 서버를 열어 둔 상태라면 LISTEN 항목을 볼 수 있고,

$ ss -f vsock -p -la | grep 19000
v_str LISTEN  0      0                  *:19000                *:*          users:(("socat",pid=19178,fd=5))

저 vsock에 연결하면 다음과 같이 ESTAB 항목이 보입니다.

$ ss -f vsock -p -la | grep 19000
v_str ESTAB   0      0                  *:19000                2:1140818933 users:(("socat",pid=19178,fd=6))

마지막으로, 아래의 글을 보면,

Hyper-V sockets internals
; https://hvinternals.blogspot.com/2017/09/hyperv-socket-internals.html

"netsh winsock show catalog" 명령으로 Hyper-V 소켓을 구현한 Layered Service Provider(LSP)를 확인할 수 있다고 합니다. (개인적으로는 LSP가 더 이상 사용하지 않는 기술이라고 여겼는데, 이런 식으로 또 보게 되는군요. ^^;)

Winsock Catalog Provider Entry
------------------------------------------------------
Entry Type:                         Base Service Provider
Description:                        Hyper-V RAW
Provider ID:                        {1234191B-4BF7-4CA7-86E0-DFD7C32B5445}
Provider Path:                      %SystemRoot%\system32\mswsock.dll
Catalog Entry ID:                   1011
Version:                            2
Address Family:                     34
Max Address Length:                 36
Min Address Length:                 36
Socket Type:                        1
Protocol:                           1
Service Flags:                      0x20026
Protocol Chain Length:              1

...[생략]...

Winsock Catalog Provider Entry
------------------------------------------------------
Entry Type:                         Base Service Provider (32)
Description:                        Hyper-V RAW
Provider ID:                        {1234191B-4BF7-4CA7-86E0-DFD7C32B5445}
Provider Path:                      %SystemRoot%\system32\mswsock.dll
Catalog Entry ID:                   1011
Version:                            2
Address Family:                     34
Max Address Length:                 36
Min Address Length:                 36
Socket Type:                        1
Protocol:                           1
Service Flags:                      0x20026
Protocol Chain Length:              1

출력에 보면, 코드에서 사용했던 "Max Address Length"와 Address Family가 각각 36, 34라는 값들과 일치합니다.

// Address Family: AF_HYPERV == 34
// Socket Type: SocketType.Stream == 1
// Protocol: HV_PROTOCOL_RAW == 1
server = new Socket(HyperVSocket.AF_HYPERV, SocketType.Stream, HyperVSocket.HV_PROTOCOL_RAW);

// Max Address Length == Marshal.SizeOf<SOCKADDR_HV>() == 36

또한, Service Flags를 해당 문서에서는 다음의 조합이라고 알려주고 있는데요,

0x20026 = XP1_GUARANTEED_DELIVERY | XP1_GUARANTEED_ORDER | XP1_GRACEFUL_CLOSE | XP1_IFS_HANDLES

아무래도 메모리를 통한 직접 전송이다 보니, "DELIVERY"와 "ORDER"가 보장이 된다는 점은 충분히 수긍이 되는 내용입니다.

그나저나, 2016년부터 ws2def.h에 AF_HYPERV 상수가 추가되었다고 하니 거의 8년이 넘는 기술이었던 것을 저는 이제야 알게 되었군요. ^^;




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







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

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

비밀번호

댓글 작성자
 




... 31  32  33  34  35  36  37  38  39  40  41  42  43  44  [45]  ...
NoWriterDateCnt.TitleFile(s)
12810정성태8/27/202115458.NET Framework: 1107. .NET Core/5+에서 동적 컴파일한 C# 코드를 (Breakpoint도 활용하며) 디버깅하는 방법 - #line 지시자파일 다운로드1
12809정성태8/26/202115439.NET Framework: 1106. .NET Core/5+에서 C# 코드를 동적으로 컴파일/사용하는 방법 [1]파일 다운로드1
12808정성태8/25/202117070오류 유형: 758. go: ...: missing go.sum entry; to add it: go mod download ...
12807정성태8/25/202117859.NET Framework: 1105. C# 10 - (9) 비동기 메서드가 사용할 AsyncMethodBuilder 선택 가능파일 다운로드1
12806정성태8/24/202114410개발 환경 구성: 601. PyCharm - 다중 프로세스 디버깅 방법
12805정성태8/24/202116115.NET Framework: 1104. C# 10 - (8) 분해 구문에서 기존 변수의 재사용 가능파일 다운로드1
12804정성태8/24/202116286.NET Framework: 1103. C# 10 - (7) Source Generator V2 APIs
12803정성태8/23/202116781개발 환경 구성: 600. pip cache 디렉터리 옮기는 방법
12802정성태8/23/202117209.NET Framework: 1102. .NET Conf Mini 21.08 - WinUI 3 따라해 보기 [1]
12801정성태8/23/202116796.NET Framework: 1101. C# 10 - (6) record class 타입의 ToString 메서드를 sealed 처리 허용파일 다운로드1
12800정성태8/22/202117173개발 환경 구성: 599. PyCharm - (반대로) 원격 프로세스가 PyCharm에 디버그 연결하는 방법
12799정성태8/22/202117447.NET Framework: 1100. C# 10 - (5) 속성 패턴의 개선파일 다운로드1
12798정성태8/21/202118809개발 환경 구성: 598. PyCharm - 원격 프로세스를 디버그하는 방법
12797정성태8/21/202116180Windows: 197. TCP의 MSS(Maximum Segment Size) 크기는 고정된 것일까요?
12796정성태8/21/202117175.NET Framework: 1099. C# 10 - (4) 상수 문자열에 포맷 식 사용 가능파일 다운로드1
12795정성태8/20/202117494.NET Framework: 1098. .NET 6에 포함된 신규 BCL API - 스레드 관련
12794정성태8/20/202116871스크립트: 23. 파이썬 - WSGI를 만족하는 최소한의 구현 코드 및 PyCharm에서의 디버깅 방법 [1]
12793정성태8/20/202117653.NET Framework: 1097. C# 10 - (3) 개선된 변수 초기화 판정파일 다운로드1
12792정성태8/19/202118755.NET Framework: 1096. C# 10 - (2) 전역 네임스페이스 선언파일 다운로드1
12791정성태8/19/202115587.NET Framework: 1095. C# COM 개체를 C++에서 사용하는 예제 [3]파일 다운로드1
12790정성태8/18/202119521.NET Framework: 1094. C# 10 - (1) 구조체를 생성하는 record struct파일 다운로드1
12789정성태8/18/202118189개발 환경 구성: 597. PyCharm - 윈도우 환경에서 WSL을 이용해 파이썬 앱 개발/디버깅하는 방법
12788정성태8/17/202115720.NET Framework: 1093. C# - 인터페이스의 메서드가 다형성을 제공할까요? (virtual일까요?)파일 다운로드1
12787정성태8/17/202116108.NET Framework: 1092. (책 내용 수정) "4.5.1.4 인터페이스"의 "인터페이스와 다형성"
12786정성태8/16/202118033.NET Framework: 1091. C# - Python range 함수 구현 (2) INumber<T>를 이용한 개선 [1]파일 다운로드1
12785정성태8/16/202116505.NET Framework: 1090. .NET 6 Preview 7에 추가된 숫자 형식에 대한 제네릭 연산 지원 [1]파일 다운로드1
... 31  32  33  34  35  36  37  38  39  40  41  42  43  44  [45]  ...