Linux 측의 socat을 이용한 Hyper-V 호스트와의 vsock 테스트
vsock을 지원하는 socat(SO cket CAT)은 1.7.4 버전부터인데요, (따라서 이것을 테스트하려면 Ubuntu의 경우 22.04 이상이 필요합니다.)
$ sudo apt install socat
자, 그럼 이걸로 Hyper-V 호스트와 리눅스 VM 간의 통신을 테스트해 볼 텐데요, 우선 호스트 측의 윈도우 프로그램은
지난번에 만들어 둔 C# 응용 프로그램으로 대체하겠습니다.
하지만, 그 예제를 그대로 사용할 수는 없고 한 가지 변경을 해야 하는데요, 바로 Service ID를 리눅스 vsock과 호환하도록 맞춰주어야 합니다. 왜냐하면, vsock의 sockaddr 구조체는,
// 버전에 따라 필드의 차이는 있지만 전체적으로 16바이트의 크기는 동일합니다.
struct sockaddr_vm {
__kernel_sa_family_t svm_family;
unsigned short svm_reserved1;
unsigned int svm_port;
unsigned int svm_cid;
__u8 svm_flags;
unsigned char svm_zero[sizeof(struct sockaddr) -
sizeof(sa_family_t) -
sizeof(unsigned short) -
sizeof(unsigned int) -
sizeof(unsigned int) -
sizeof(__u8)];
};
Guid를 사용하는 윈도우(Hyper-V Socket)와 달리 "unsigned int" 타입의 svm_cid, svm_port를 쓰기 때문입니다. 이러한 차이점을 메우기 위해, 윈도우 측에서는 vsock과 호환하기 위해 특별한 GUID를 소개하고 있습니다.
// Hyper-V Socket Linux guest VSOCK template GUID
struct __declspec(uuid("00000000-facb-11e6-bd58-64006a7986d3")) VSockTemplate{};
/*
* GUID example = __uuidof(VSockTemplate);
* example.Data1 = 2761; // 0x00000AC9
*/
즉, "00000000-facb-11e6-bd58-64006a7986d3" 값을 써야 하고, 앞 부분의 "00000000" 측에 svm_port에 해당하는 값을 16진수로 쓰는 식입니다. 가령, 19000 포트를 쓰고 싶다면 그것의 16진수 값인 0x4a38을 적용해 "00004A38-facb-11e6-bd58-64006a7986d3" 값으로 해야 하는 것입니다.
마지막으로, 저 값을 "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization\GuestCommunicationServices" 하위에 등록하는 것을 잊지 마시고.
실습을 위해, 이제
지난번 소스코드에서 GcsGuid 값만 바꿔주고,
// 19000번 포트를 사용한다고 가정
public static readonly Guid GcsGuid = new Guid("00004A38-facb-11e6-bd58-64006a7986d3");
(레지스트리 등록 잊지 마시고) 빌드 후 서버 모드로 실행해 둔 후,
ConsoleApp1.exe /server any
Hyper-V에 설치한 Ubuntu 22.04 VM에서 socat을 이용해 다음과 같이 테스트할 수 있습니다.
// socat 실행 후, "test" 입력하고 enter 키를 누르면 ConsoleApp1.exe 측에서 반환하는 "Hello: test"가 출력됩니다.
$ socat - VSOCK-CONNECT:2:19000
test
Hello: test
$
VSOCK-CONNECT 인자와 함께 "2:19000" 값을 주었는데요, 여기서 2는 VMADDR_CID_HOST로 정의된 상수입니다.
/* Use this as the destination CID in an address when referring to the host
* (any process other than the hypervisor). VMCI relies on it being 2, but
* this would be useful for other transports too.
*/
#define VMADDR_CID_HOST 2
즉, VM에서 호스트 측으로 연결 시 svm_cid 필드에 설정해야 하는 값으로, Hyper-V Socket의 "HV_GUID_PARENT(a42e7cda-d03f-480c-9cc2-a4de20abb878)"에 해당한다고 보면 됩니다.
리눅스 측의 vsock 연결을 클라이언트로 했으니 이제 반대로 서버로써도 테스트해야겠죠? ^^
이를 위해 socat을 서버 모드로 실행해 두는데요,
$ socat - VSOCK-LISTEN:19000
역으로 Hyper-V 호스트 측에서는 (전과 다름없이) VM ID를 통해 접속하면 됩니다.
// socat을 실행한 Linux VM의 VM ID가 "f952c5ef-ea6c-4c83-b52c-38f31cf68b61"인 경우,
ConsoleApp1.exe /client f952c5ef-ea6c-4c83-b52c-38f31cf68b61
ConsoleApp1.exe 코드에서는 어차피 Service ID를 "0000
4A38-facb-11e6-bd58-64006a7986d3"로 사용해 19000 포트로 연결할 것이므로, 결국 필요한 것은 VM ID뿐입니다.
참고로, 윈도우의 경우 Server 측 바인딩이 HV_GUID_WILDCARD와 HV_GUID_LOOPBACK이 가능했는데요, 리눅스도 그와 유사하게 VMADDR_CID_ANY와 (한때 VMADDR_CID_RESERVED였던) VMADDR_CID_LOCAL이 있습니다.
/* The vSocket equivalent of INADDR_ANY. This works for the svm_cid field of
* sockaddr_vm and indicates the context ID of the current endpoint.
*/
#define VMADDR_CID_ANY -1U
/* Use this as the destination CID in an address when referring to the
* local communication (loopback).
* (This was VMADDR_CID_RESERVED, but even VMCI doesn't use it anymore,
* it was a legacy value from an older release).
*/
#define VMADDR_CID_LOCAL 1
socat이 어떤 값과 바인딩하는지는 모르겠지만, 위의 설명만 봐서는 VMADDR_CID_ANY인 듯합니다. 이런 경우, 역시나 윈도우에서처럼 같은 VM 내에서는 VMADDR_CID_LOCAL로 연결할 수 있는데요, 따라서 같은 VM 내에서 2개의 터미널을 열어두고 각각 다음과 같이 테스트하면 서로 연결이 됩니다.
// 리눅스 VM의 터미널 A
$ socat - VSOCK-LISTEN:19000
// 리눅스 VM의 터미널 B
$ socat - VSOCK-CONNECT:1:19000
"1:19000"에서 "1"은 VMADDR_CID_LOCAL 값에 해당합니다. 그런 의미에서 본다면 VMADDR_CID_ANY와 VMADDR_CID_LOCAL은 Hyper-V Socket으로 매핑한다면 각각 HV_GUID_WILDCARD와 HV_GUID_LOOPBACK이 됩니다.
참고로, Linux 측에서 vsock 지원 여부를 모듈 열거를 통해 확인할 수 있다고 합니다.
$ lsmod | grep vsock
vsock_loopback 12288 0
vmw_vsock_virtio_transport_common 61440 1 vsock_loopback
vmw_vsock_vmci_transport 45056 0
vsock 61440 4 vmw_vsock_virtio_transport_common,vsock_loopback,hv_sock,vmw_vsock_vmci_transport
vmw_vmci 106496 1 vmw_vsock_vmci_transport
또한, 윈도우의 Hyper-V에서 호스팅하는 것이 아닌, Linux를 호스트로 Linux VM을 돌리는 경우에는 svm_cid 값을 VM 시작 시의 인자로 전달해야 합니다. 가령 qemu의 경우,
// https://github.com/stefano-garzarella/socat-vsock/blob/be46eaf169eea1c63b3d90772610a8733a346e30/EXAMPLES#L236
//////////////////////////////////////////////////////////////////////////////
// VSOCK
# start a linux VM with cid=21
# qemu-system-x86_64 -m 1G -smp 2 -cpu host -M accel=kvm \
# -drive if=virtio,file=/path/to/fedora.img,format=qcow2 \
# -device vhost-vsock-pci,guest-cid=21
# guest listens on port 1234 and host connects to it
guest$ socat - VSOCK-LISTEN:1234
host$ socat - VSOCK-CONNECT:21:1234
# host (well know CID_HOST = 2) listens on port 4321 and guest connects to it
host$ socat - VSOCK-LISTEN:4321
guest$ socat - VSOCK-CONNECT:2:4321
# ssh over vsock (guest-cid = 21)
guest$ socat VSOCK-LISTEN:22,reuseaddr,fork TCP:localhost:22
host$ socat TCP4-LISTEN:22222,reuseaddr,fork VSOCK-CONNECT:21:22
host$ ssh -p 22222 user@localhost
guest-cid 값을 통해 CID를 지정할 수 있습니다.
마지막으로, vsock이 지원되면 /dev/vsock 파일이 열거되는데요,
$ ls /dev/vsock -l
crw-rw-rw- 1 root root 10, 118 7월 2 09:33 /dev/vsock
이 파일에 대해 IOCTL_VM_SOCKETS_GET_LOCAL_CID 명령을 보내면 (qemu의 경우 guest-cid 값으로 전달한) CID 값을 얻는 것이 가능합니다.
#!/usr/bin/env python3
import socket
import struct
import fcntl
with open("/dev/vsock", "rb") as fd:
r = fcntl.ioctl(fd, socket.IOCTL_VM_SOCKETS_GET_LOCAL_CID, " ")
cid = struct.unpack("I", r)[0]
print("Local CID: {}".format(cid))
# https://gist.github.com/stefano-garzarella/b4971c2bed3c632b48c5e2a823c1cbe1
(qemu 등으로 guest-cid를 설정하지 않은) VM 또는 호스트의 경우에는 CID 값이 4294967295로 나옵니다.
Ubuntu 20.04에서 기본 설치하는 socat은,
$ apt show socat
Package: socat
Version: 1.7.3.3-2
...[생략]...
$ sudo apt install socat
1.7.3 버전인데요, vsock을 지원하지 않습니다.
SOCAT now supports AF_VSOCK
; https://stefano-garzarella.github.io/posts/2021-01-22-socat-vsock/
22.04 우분투 repo의 deb 파일을 직접 다운로드해,
socat_1.7.4.1-3ubuntu4_amd64.deb
; https://ubuntu.pkgs.org/22.04/ubuntu-main-amd64/socat_1.7.4.1-3ubuntu4_amd64.deb.html
; http://archive.ubuntu.com/ubuntu/pool/main/s/socat/socat_1.7.4.1-3ubuntu4_amd64.deb
설치하면,
$ wget http://archive.ubuntu.com/ubuntu/pool/main/s/socat/socat_1.7.4.1-3ubuntu4_amd64.deb
$ sudo apt install ./socat_1.7.4.1-3ubuntu4_amd64.deb
이런 오류가 발생할 것입니다.
$ sudo apt install ./socat_1.7.4.1-3ubuntu4_amd64.deb
...[생략]...
The following packages have unmet dependencies:
socat : Depends: libc6 (>= 2.34) but 2.31-0ubuntu9.16 is to be installed
Depends: libssl3 (>= 3.0.0~~alpha1) but it is not installable
E: Unable to correct problems, you have held broken packages.
아마도 저게 동작하려면, socat
빌드를 glibc 2.34 버전을 대상으로, libssl3도 함께 빌드해 링크해야 할 것입니다. 저야 리눅스 초보자라... ^^;
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]