Разбираемся с Vsock
Vsock - это семейство сокетов в Linux, предназначенное для связи между виртуальной машиной и её гипервизором.
Это новое семейство сокетов обеспечивает двустороннее, «многие-к-одному», взаимодействие между гипервизором и виртуальными машинами.
Изначально добавлено VMware напрямую в ядро.
Также может использоваться в QEMU/KVM, а также в Hyper-V, но там код уже закрытый.
Пользовательские приложения - как внутри виртуальной машины, так и на хосте - могут использовать API VM Sockets, которое обеспечивает быструю и эффективную коммуникацию между гостевыми ВМ и хостом. Предоставляется семейство адресов сокетов, на уровне интерфейса совместимое с UDP и TCP.
Модуль VM Sockets поддерживает как ориентированные на соединение потоковые сокеты, аналогичные TCP, так и неориентированные датаграммные сокеты, аналогичные UDP. Протокольное семейство VM sockets определено как AF_VSOCK, а операции сокетов разделены для SOCK_DGRAM и SOCK_STREAM.
Поскольку VM sockets вообще не используют сетевой стек хоста, виртуальные машины можно настраивать полностью без сетевого интерфейса - разрешая взаимодействие только через VM sockets.
Хост и каждая ВМ имеют 32-битный CID (Context IDentifier) и могут подключаться или выполнять bind к 32-битному номеру порта. Порты с номерами меньше 1024 считаются привилегированными.

Адреса VM sockets
Context IDentifier хранятся в файле /dev/vsock.
Приложение использует <CID>:<port> в качестве адреса сокета.
CID похож на IP-адрес и представлен 32-битным целым числом.
Он идентифицирует либо гипервизор, либо виртуальную машину. Часть адресов зарезервирована - включая 0, 1 и максимальное значение для 32-битного целого: 0xffffffff.
Каждая виртуальная машина должна иметь уникальный CID.
Гипервизору всегда назначается CID 2, а виртуальные машины могут использовать значения от 3 до 0xffffffff - 1.
/* Use this as the destination CID in an address when referring to the * hypervisor. VMCI relies on it being 0, but this would be useful for other * transports too. */ #define VMADDR_CID_HYPERVISOR 0 /* This CID is specific to VMCI and can be considered reserved (even VMCI * doesn't use it anymore, it's a legacy value from an older release). */ #define VMADDR_CID_RESERVED 1 /* 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 In [1]: import socket In [2]: socket.VMADDR_CID_HOST Out[2]: 2
Порт также аналогичен TCP-порту и представлен 32-битным целым числом.
void vsock_addr_init(struct sockaddr_vm *addr, u32 cid, u32 port)
{
memset(addr, 0, sizeof(*addr));
addr->svm_family = AF_VSOCK;
addr->svm_cid = cid;
addr->svm_port = port;
}
Как и в случае с IP-портами, порты в диапазоне 0–1023 считаются «привилегированными», и выполнять bind к ним может только root или пользователь с правом CAP_NET_ADMIN.
По умолчанию CID и порт могут быть сгенерированы ядром случайным образом.
/* 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 /* Bind to any available port. Works for the svm_port field of * sockaddr_vm. */ #define VMADDR_PORT_ANY -1U
Несколько приложений могут работать на одном и том же хосте, используя разные порты, и каждый порт при этом способен обслуживать несколько соединений одновременно.

Чтобы получить локальный CID в Go:
package main
import (
"fmt"
"os"
"golang.org/x/sys/unix"
)
// contextID retrieves the local context ID for this system.
var devVsock = "/dev/vsock"
func main(){
fmt.Println(contextID())
}
func contextID() (uint32, error) {
f, err := os.Open(devVsock)
if err != nil {
return 0, err
}
defer f.Close()
return unix.IoctlGetUint32(int(f.Fd()), unix.IOCTL_VM_SOCKETS_GET_LOCAL_CID)
}
Настройка VM socket
Чтобы vsock нормально завёлся, нужен достаточно свежий kernel (>~4.8) и в QEMU-виртуалке, и на хосте, плюс для запуска ВМ требуется QEMU 2.8+.
Также нужно, чтобы были включены модули vsock и vhost:
➭ lsmod | grep vsock vhost_vsock 24576 0 vmw_vsock_virtio_transport_common 32768 1 vhost_vsock vsock 36864 2 vmw_vsock_virtio_transport_common,vhost_vsock vhost 49152 2 vhost_vsock,vhost_net ➭ file /dev/vsock /dev/vsock: character special (10/59) ➭ file /dev/vhost-vsock /dev/vhost-vsock: character special (10/241)
Создание QEMU виртуальной машины
Сначала создаём пустой образ диска:
qemu-img create debian.img 2G
Дальше грузимся с нужного .iso:
qemu-system-x86_64 -hda debian.img -cdrom debian-testing-amd64-netinst.iso -boot d -m 512
Откроется установщик Debian.
После установки систему можно загрузить так:
qemu-system-x86_64 -hda debian.img -m 512 -enable-kvm -device vhost-vsock-pci,id=vhost-vsock-pci0,guest-cid=123
Это запустит Debian VM с устройством vsock и CID равным 123.
Устройство vhost-vsock-pci подключается к виртуалке и включает связь с хостом. После запуска ВМ у вас должен появиться доступный vsock-девайс.

Примечание: в виртуальной машине разрешён только один vsock-девайс - гостевые драйверы virtio-vsock не поддерживают несколько экземпляров: https://bugzilla.redhat.com/show_bug.cgi?id=1455015Отправка и приём vsock-пакетов
В QEMU-виртуалке вы можете отправлять vsock-пакеты после того, как включите vsock-устройство.
Дальше коммуникация настраивается уже логически - на уровне языка программирования:

Python
Гипервизор - receive.py
#!/usr/bin/env python3
import socket
CID = socket.VMADDR_CID_HOST
PORT = 9999
s = socket.socket(socket.AF_VSOCK, socket.SOCK_STREAM)
s.bind((CID, PORT))
s.listen()
(conn, (remote_cid, remote_port)) = s.accept()
print(f"Connection opened by cid={remote_cid} port={remote_port}")
while True:
buf = conn.recv(64)
if not buf:
break
print(f"Received bytes: {buf}")
Виртуальная машина - send.py
#!/usr/bin/env python3 import socket CID = socket.VMADDR_CID_HOST PORT = 9999 s = socket.socket(socket.AF_VSOCK, socket.SOCK_STREAM) s.connect((CID, PORT)) s.sendall(b"Hello, world!") s.close()
Вывод на хосте:
Connection opened by cid=123 port=2131488197 Received bytes: b'Hello, world!'
При запуске виртуальной машины мы задали CID равным 123 - и можем увидеть эту информацию на стороне гипервизора.
Мониторинг vsock-пакетов
Благодаря ребятам из Google у нас есть инструменты для мониторинга vsock-коммуникации.
Для начала нужно создать линк типа vsockmon:
ip link add type vsockmon ip link set vsockmon0 up
Тип линка vsockmon предоставляется модулем vsockmon:

Можно собрать кастомный бинарник tcpdump с поддержкой vsock-фильтра или просто воспользоваться Wireshark.
wireshark -k -i vsockmon0

Парсер vsock
https://www.tcpdump.org/linktypes/LINKTYPE_VSOCK.html
import socket,sys,select,json [20/240]
import struct
s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.ntohs(3))
s.bind(("vsockmon0",3))
operations_type = {
1:"Connect",
2:"Disconnect",
3:"Control",
4:"Payload"
}
transports_type = {
1:"1",
2:"Virtio"
}
socket_field_type = {
1:"1",
2:"Stream"
}
operations_vt__type = {
1:"Conn Request",
2:"Conn Response",
3:"Conn Reset",
4:"Conn Shutdown",
5:"Data Packet",
6:"Credit Update",
7:"Credit Update Request"
}
def vsock_parser(raw_data):
vsock = {}
src_cid,dst_cid, src_port,dst_port,operation, transport_type, transport_length = struct.unpack('< Q Q L L H H H 2x',raw_data[:32
])
vsock.extend([src_cid,dst_cid, src_port,dst_port,operations_type[operation],transports_type[transport_type],transport_length])
data = raw_data[32:]
vsock["Source CID"] = src_cid
vsock["Destination CID"] = dst_cid
vsock["Source Port"] = src_port
vsock["Destination Port"] = dst_port
vsock["Operation"] = operations_type[operation]
vsock["Transport Type"] = transports_type[transport_type]
vsock["Transport Length"] = transport_length
return vsock,data
def virtio_parser(raw_data, length):
virtio = {}
src_cid,dst_cid, src_port,dst_port,payload_length,conn_type,operation, flag,buffer_alloc,fwd_count = struct.unpack('< Q Q L L I H
H I I I',raw_data[:length])
virtio.extend([src_cid,dst_cid, src_port,dst_port,payload_length,socket_field_type[conn_type],operations_vt__type[operation], fl
ag,buffer_alloc,fwd_count])
virtio["Payload Length"] = payload_length
virtio["Type"] = socket_field_type[conn_type]
virtio["Operation"] = operations_vt__type[operation]
virtio["Flags"] = flag
virtio["Buffer Alloc"] = buffer_alloc
virtio["Receive Counter"] = fwd_count
data = raw_data[length:]
if operation == 5:
payload = struct.unpack(f'< {payload_length}c',data)
payload_str = [ x.decode('unicode_escape') for x in payload ]
payload = "".join(payload_str)
virtio["Payload"] = payload
virtio.append(payload)
virtio.append(struct.unpack(f'< {payload_length}c',data))
return virtio
Транспорт для нескольких ВМ
Есть несколько способов заставить ВМ общаться через vsock - либо в сценарии с вложенной виртуализацией (nested VM), либо для локального взаимодействия.
Red Hat предложили способ закрыть оба случая в ядре Linux v5.6.

В этом примере первый гость использует оба типа транспорта: H2G - Host to Guest - и G2H - Guest to Host. За счёт этого гипервизор может отправлять сообщения во вложенного гостя через первую ВМ.

Локальное взаимодействие без ВМ появилось в Linux v5.6: приложения могут обращаться друг к другу по локальному CID или по локальному guest CID, если включён G2H.
Socat multicast: https://github.com/stefano-garzarella/socat-vsock/blob/vsock/doc/socat-multicast.html
Связь между «самостоятельными» ВМ
Если вы хотите, чтобы несколько ВМ общались через vsock, но при этом они не вложенные (не nested), придётся поднять прокси на гипервизоре.
Этот пример завязан на JSON-конфиг, в котором описаны сервисы.
#!/usr/bin/python3
import socket, sys, select, json
def create_socket(port):
CID = socket.VMADDR_CID_HOST
s = socket.socket(socket.AF_VSOCK, socket.SOCK_STREAM)
s.bind((socket.VMADDR_CID_ANY, port))
s.listen()
return s
def inc(x):
return x + 1
def main():
read_list = []
raddr = []
laddr = []
with open("configuration.json","r") as c:
sd = json.load(c)
for i in sd:
for att in i["Services"]:
read_list.append(create_socket(att["port"]))
for i in read_list:
laddr.append(i.getsockname()[0])
while True:
read,write,exec = select.select(read_list, [], read_list)
for so in read:
if so.getsockname()[0] not in laddr:
raddr.append(so)
if so in raddr:
conn,(remote_cid,remote_port) = so.accept()
print(conn)
print(f"Connection opened by cid={remote_cid} port={conn.getsockname()[1]}")
payload = conn.recv(1024)
if not payload:
break
elif payload == "killsrv":
conn.close()
sys.exit()
else:
for x in sd:
for y in x["Services"]:
if conn.getsockname()[1] == y["port"]:
vm_port = y["port"]
vm_cid = x["CID"]
s = socket.socket(socket.AF_VSOCK, socket.SOCK_STREAM)
s.connect((vm_cid,vm_port))
s.sendall(payload)
print(f"Payload sent to cid={vm_cid} port={vm_port} and data={payload}")
s.close()
if __name__ == "__main__":
main()
configuration.json:
[
{
"CID": 123,
"Services": [
{
"id": 34,
"name": "Service 1",
"port": 9999
}
]
},
{
"CID": 456,
"Services": [
{
"id": 35,
"name": "Service 2",
"port": 1111
}
]
},
{
"CID": 789,
"Services": [
{
"id": 36,
"name": "Service 3",
"port": 2222
}
]
}
]
Источники
https://terenceli.github.io/技术/2020/04/18/vsock-internals
https://static.sched.com/hosted_files/devconfcz2020a/b1/DevConf.CZ_2020_vsock_v1.1.pdf
https://github.com/torvalds/linux/commit/c0cfa2d8a788fcf45df5bf4070ab2474c88d543a