使用 Vsock 与 Libzmq
摘要
本文介绍如何将用于虚拟机通信的 Linux 套接字地址族 VSOCK 与 libzmq 和 pyzmq 集成,包括优点和实现细节。
暂无内容
查看缓存全文
缓存时间: 2026/09/10 02:25
# 使用 libzmq 实现 VSOCK 通信
来源:https://blog.remijouan.net/posts/libzmq-vsock-pyzmq/
## AF_VSOCK
VSOCK 并非新技术,自 Linux 4.8 内核起便已存在。对于不了解此技术的用户(https://www.youtube.com/watch?v=QzoBB9NO9Dc),其前身是 VMware 开发的 VMCI。AF_VSOCK 是一种套接字地址族,类似于 AF_INET 或 AF_UNIX。该地址族专为虚拟机(客户机)与底层虚拟机管理程序(主机)之间的通信而设计,也可用于构建同一主机上多个客户机之间的通信。过去,此类通信需通过串口实现,典型应用案例可见 https://pve.proxmox.com/wiki/Qemu-guest-agent。AF_VSOCK 的行为模式与 Unix 套接字类似,兼具 TCP/UDP 特性:支持地址与端口配置,同时提供流式套接字与数据报套接字两种通信模式。
详见手册页 https://man7.org/linux/man-pages/man7/vsock.7.html
地址采用 32 位“上下文标识符”(Context Identifier),包含以下保留 CID:VMADDR_CID_ANY、VMADDR_CID_HYPERVISOR、VMADDR_CID_LOCAL(5.6 内核新增)、VMADDR_CID_HOST。端口号同样为 32 位,1024 以下端口需要 root 权限。与 TCP/UDP 类似,可通过不同端口在同一 CID 上建立多条通信链路。
VSOCK 已存在一段时间,但支持生态逐步完善。Python/C/Golang/Rust 均提供支持,并附有基础工具及 SDK:
- https://stefano-garzarella.github.io/posts/2021-01-22-socat-vsock/
- https://github.com/rust-vsock/tokio-vsock
- https://mdlayher.com/blog/linux-vm-sockets-in-go/
- https://docs.python.org/3/library/socket.html#socket.AF_VSOCK
- https://gitlab.com/vsock/vsock
AWS 的 Nitro Enclaves 功能也采用了该技术:https://docs.aws.amazon.com/enclaves/latest/user/developing-applications-linux.html
## libzmq 中的支持情况
若计划使用 vsock,您会发现目前仅有底层绑定实现,缺乏对套接字进行抽象封装的便捷库。您可能已很久未直接操作套接字,需要重新回忆 poll、recv、send、open、listen、bind 等操作流程。幸运的是,https://zeromq.org/ 提供了优秀的解决方案:该库基于各类套接字实现了基础通信模式与最佳实践,甚至包含安全特性(http://curvezmq.org/)。
令人惊喜的是,libzmq 已有 VMCI 实现(https://libzmq.readthedocs.io/en/latest/zmq_vmci.html),但尚未支持 VSOCK。因此我直接参照 VMCI 代码提出了实现方案(https://github.com/zeromq/libzmq/pull/4822)。在 libzmq 中集成 VSOCK 将带来显著优势:
- 所有支持 libzmq 绑定的语言(Python、Ruby、Node.js、Perl、Java、Lua 等)均可使用 AF_VSOCK
- 可通过 libzmq 在 AF_VSOCK 上使用高级特性:Curve 认证、多种消息模式(req/rep、pub/sub 等)
libzmq 因稳定性较高且迭代较慢,并不频繁发布新版本。同时作为嵌入式友好型库,支持基于特定提交的静态编译。VSOCK 功能终将纳入稳定版,目前我已 fork pyzmq 并集成最新 libzmq 提交以供使用:https://github.com/remijouannet/pyzmq-vsock。
libzmq vsock 文档:https://github.com/zeromq/libzmq/blob/master/doc/zmq_vsock.adoc
## pyzmq-vsock 示例
### Hello World
自 Linux 5.6 起支持回环 CID(VMADDR_CID_LOCAL),无需虚拟机即可测试 VSOCK。在 ZMQ 中使用 @ 符号可直接绑定到回环地址。以下为基于 VSOCK 回环的 req/rep 套接字基础示例。
若要在真实宿主/客户机环境中测试 vsock,需在 QEMU 命令行中添加 vsock 设备(或相应 libvirt 配置 https://libvirt.org/formatdomain.html#vsock):
```bash
export CID=100
/usr/local/bin/qemu-system-x86_64 \
... \
-device vhost-vsock-pci,id=vhost-vsock-pci0,guest-cid=$CID \
...
```
```bash
# 可能需要加载回环模块:sudo modprobe vsock_loopback
python3 -m venv venv
venv/bin/pip install \
https://github.com/remijouannet/pyzmq-vsock/releases/download/27.2.0.dev0%2B4649337/pyzmq-27.2.0.dev0+4649337-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
# 基于 https://zeromq.org/get-started/?language=python# 改编的 vsock 示例
cat < rep.py
#
# Python 实现的 Hello World 服务端
# 绑定 REP 套接字至 vsock://@:5555
# 接收客户端发送的 b"Hello",回复 b"World"
#
import time
import zmq
context = zmq.Context()
socket = context.socket(zmq.REP)
socket.bind("vsock://@:5555")
while True:
# 等待客户端请求
message = socket.recv()
print(f"收到请求: {message}")
# 执行"处理"操作
time.sleep(1)
# 发送响应给客户端
socket.send(b"World")
EOF
cat < req.py
#
# Python 实现的 Hello World 客户端
# 连接 REQ 套接字至 vsock://@:5555
# 向服务端发送"Hello",等待返回"World"
#
import zmq
context = zmq.Context()
# 与服务端通信的套接字
print("正在连接 hello world 服务端...")
socket = context.socket(zmq.REQ)
socket.connect("vsock://@:5555")
# 发送 5 次请求,每次等待响应
for request in range(5):
print(f"发送请求 {request} ...")
socket.send(b"Hello")
# 接收响应
message = socket.recv()
print(f"收到响应 {request} [ {message} ]")
EOF
venv/bin/python3 rep.py &
[1] 88069
venv/bin/python3 req.py
正在连接 hello world 服务端...
发送请求 0 ...
收到请求: b'Hello'
收到响应 0 [ b'World' ]
发送请求 1 ...
收到请求: b'Hello'
收到响应 1 [ b'World' ]
发送请求 2 ...
收到请求: b'Hello'
收到响应 2 [ b'World' ]
发送请求 3 ...
收到请求: b'Hello'
收到响应 3 [ b'World' ]
发送请求 4 ...
收到请求: b'Hello'
收到响应 4 [ b'World' ]
```
### Curve 与 asyncio
Hello World 示例仅为演示,实际生产环境通常需结合 asyncio 与 Curve 安全协议。以下示例改编自 pyzmq(https://github.com/zeromq/pyzmq/blob/main/examples/security/asyncio-ironhouse.py)。
**Curve 密钥生成脚本**
```bash
#!/usr/bin/env python
import json
import zmq
import zmq.auth
keys_file = "keys.json"
client_pub, client_priv = zmq.curve_keypair()
client2_pub, client2_priv = zmq.curve_keypair()
server_pub, server_priv = zmq.curve_keypair()
with open(keys_file, "w") as f:
json.dump(
{
"client": [client_pub.decode(), client_priv.decode()],
"client2": [client2_pub.decode(), client2_priv.decode()],
"server": [server_pub.decode(), server_priv.decode()],
},
f,
sort_keys=True,
indent=4,
)
```
**REP 服务端**
```bash
#!/usr/bin/env python
import asyncio
import json
import logging
import zmq
import zmq.auth
from zmq.asyncio import Context
from zmq.auth.asyncio import AsyncioAuthenticator
LOGGER = logging.getLogger(__name__)
async def run(keys: dict) -> None:
ctx = Context.instance()
# 为该上下文启动认证器
auth = AsyncioAuthenticator(ctx)
auth.start()
auth.certs["*"] = {keys["client"][0].encode(): True}
server = ctx.socket(zmq.REP)
server.curve_publickey = zmq.utils.z85.decode(keys['server'][0])
server.curve_secretkey = zmq.utils.z85.decode(keys['server'][1])
server.curve_server = True # 必须在 bind 之前设置
server.bind('vsock://@:9000')
msg = await server.recv()
LOGGER.info(f"收到 {msg!r}")
if msg == b"Hello":
LOGGER.info("Ironhouse 测试通过")
await server.send(b"World")
# 关闭套接字
server.close()
auth.stop()
if __name__ == '__main__':
if not zmq.has("vsock") or not zmq.has("curve"):
raise RuntimeError(
f"libzmq 版本 < 4.0 不支持安全特性。当前版本:{zmq.zmq_version()}"
)
level = logging.DEBUG
logging.basicConfig(level=level, format="[%(levelname)s] %(message)s")
with open("keys.json") as f:
keys = json.load(f)
asyncio.run(run(keys))
```
**REQ 客户端**
```bash
#!/usr/bin/env python
import asyncio
import json
import logging
import zmq
import zmq.auth
from zmq.asyncio import Context
from zmq.auth.asyncio import AsyncioAuthenticator
LOGGER = logging.getLogger(__name__)
async def run(keys: dict) -> None:
ctx = Context.instance()
# 为该上下文启动认证器
auth = AsyncioAuthenticator(ctx)
auth.start()
client = ctx.socket(zmq.REQ)
client.curve_publickey = zmq.utils.z85.decode(keys['client'][0])
client.curve_secretkey = zmq.utils.z85.decode(keys['client'][1])
client.curve_serverkey = zmq.utils.z85.decode(keys['server'][0])
client.connect('vsock://@:9000')
await client.send(b"Hello")
reply = await client.recv()
LOGGER.info(f"收到响应 {reply!r}")
client.close()
auth.stop()
if __name__ == '__main__':
if not zmq.has("vsock") or not zmq.has("curve"):
raise RuntimeError(
f"libzmq 版本 < 4.0 不支持安全特性。当前版本:{zmq.zmq_version()}"
)
level = logging.DEBUG
logging.basicConfig(level=level, format="[%(levelname)s] %(message)s")
with open("keys.json") as f:
keys = json.load(f)
asyncio.run(run(keys))
```
**认证成功时的输出**
```bash
venv/bin/python generate_keys.py
venv/bin/python rep_asyncio_curve.py
[DEBUG] 使用选择器: EpollSelector
[DEBUG] 启动
[DEBUG] version: b'1.0', request_id: b'1', domain: '', address: '', identity: b'', mechanism: b'CURVE'
[DEBUG] ALLOWED (CURVE) domain=* client_key=b'HhdIwzo4=a}1F#eL{}rs4C1Hgx.Z4nd#/JqIasmP'
[DEBUG] ZAP reply code=b'200' text=b'OK'
[INFO] 收到 b'Hello'
[INFO] Ironhouse 测试通过
venv/bin/python req_asyncio_curve.py
[DEBUG] 使用选择器: EpollSelector
[DEBUG] 启动
[INFO] 收到响应 b'World'
```
**使用未授权密钥时的输出**
```bash
venv/bin/python generate_keys.py
venv/bin/python rep_asyncio_curve.py
[DEBUG] 使用选择器: EpollSelector
[DEBUG] 启动
[DEBUG] version: b'1.0', request_id: b'1', domain: '', address: '', identity: b'', mechanism: b'CURVE'
[DEBUG] DENIED (CURVE) domain=* client_key=b'P4//#^+&*nKTcb]6*u:zy
```
相似文章
SOCKMAP - 未来的TCP拼接
Cloudflare 探索利用Linux内核的SOCKMAP基础设施进行TCP套接字拼接,从而减少用户空间开销,并实现更高效的反向代理数据转发。
Zig 0.16 中的异步 I/O:今日视角
Zig 0.16 推出了新的 std.Io 接口,用于跨平台 I/O。zio 库通过栈式协程和操作系统级异步 API 提供了完整的异步实现,无需每个任务一个线程即可实现高效的并发任务。
为套接字接口喝彩
这篇文章回顾了1983年发布的BSD 4.2套接字接口的历史影响,该接口标准化了网络访问,为现代在线服务铺平了道路。
Zig 的 Io.Threaded 很巧妙
文章讨论了 Zig 的 std.Io.Threaded,这是 Zig Io 接口的一种实现,使用阻塞系统调用并通过信号支持取消,同时对比了并发与并行。
在Proxmox VE中运行microVM的简便方法
介绍了pve-microvm,这是一个Debian软件包,它将QEMU的microvm机器类型集成到Proxmox VE中,实现了低于300毫秒的启动时间和硬件隔离,开销极小,支持多种客户操作系统。