プログラミング
LibzmqでVsockを使用する
Use Vsock with Libzmq (blog.remijouan.net)
要約
この記事は、VM(仮想マシン)とホスト間、または同一ホスト上のVM間通信に利用されるVSOCK(Virtual Socket)について解説しています。libzmqライブラリにVSOCKサポートが追加されたことで、この低レベルな通信メカニズム上で、認証や様々なメッセージングパターンといったlibzmqの高度な機能を利用できるようになりました。筆者は、pyzmqを用いたVSOCKの基本的な使用例や、asyncioとCurve認証を用いたセキュリティ機能の実装例を紹介しています。
全文翻訳
VSOCKをLibzmqで使用する
AF_VSOCK
VSOCKは新しいものではなく、Linuxカーネルには既に4.8から存在しています。知らない方のために説明すると、以前はVMwareによってVMCIという名前で開発されていました。AF_VSOCKは、AF_INETやAF_UNIXのようなソケットアドレスファミリーです。これは、VM(ゲスト)と基盤となるハイパーバイザー(ホスト)間の通信、あるいは同じホスト上の複数のゲスト間の通信を構築するために設計されたアドレスファミリーです。以前は、このような通信はシリアルポートで行われており、その良い例がhttps://pve.proxmox.com/wiki/Qemu-guest-agentです。AF_VSOCKは、Unixソケットに似た感覚で、TCP/UDPのような機能を持っています。アドレスとポートがあり、ストリームとデータグラムも利用できます。マニュアルページはhttps://man7.org/linux/man-pages/man7/vsock.7.htmlです。VMADDR_ANY、VMADDR_HYPERVISOR、VMADDR_LOCAL(5.6で新設)、VMADDR_HOSTといった予約済みのCID(コンテキスト識別子)を持つ32ビットアドレスがあります。ポート番号に関しては、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/)さえ備えています。驚いたことに、VMCIの実装(https://libzmq.readthedocs.io/en/latest/zmq_vmci.html)はありましたが、VSOCKはありませんでした。提案は簡単でした。VMCIのコードをほぼコピー&ペーストしただけです。https://github.com/zeromq/libzmq/pull/4822
libzmqにVSOCKが含まれることには多くの利点があります。
Python、Ruby、Node.js、Perl、Java、Luaなど、libzmqバインディングを持つあらゆる言語でAF_VSOCKを使用できます。
AF_VSOCK上でlibzmqの機能を使用できます。Curveによる認証、異なるメッセージパターン、req/rep、pub/sub。
libzmqは、安定しておりあまり変更されないため、定期的なリリースを行いません。また、組み込みソフトウェア向けのライブラリであり、特定のコミットに静的にビルドしやすいです。VSOCKは、いつか安定版リリースに含まれることになるでしょう。それまでの間、最新のlibzmqコミットでビルドするためにpyzmqをフォークしました。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)が追加されたため、VMを実行せずにVSOCKをテストできます。ZMQの@記号を使用して、このループバックに直接バインドします。以下は、VSOCKループバック上でreq/repソケットペアを使用する基本的な例です。
ゲストとホストでVSOCKを実際にテストしたい場合は、QEMUコマンドラインにvsockデバイスを追加する必要があります(またはlibvirtの設定でhttps://libvirt.org/formatdomain.html#vsock)。
1export CID=100
2/usr/local/bin/qemu-system-x86_64 \
3...
4 -device vhost-vsock-pci,id=vhost-vsock-pci0,guest-cid=$CID
5...
1
2# vsock_loopbackはおそらくお使いのマシンにはロードされていません
3# sudo modprobe vsock_loopback
4
5python3 -m venv venv
6
7venv/bin/pip install \
8 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
9
10# https://zeromq.org/get-started/?language=python# からの例をvsock用に適応させました
11cat <<EOF > rep.py
12#
13# PythonでのHello Worldサーバー
14# vsock://@:5555にREPソケットをバインドします
15# クライアントからb"Hello"を受け取り、b"World"で応答します
16#
17import time
18import zmq
19
20context = zmq.Context()
21socket = context.socket(zmq.REP)
22socket.bind("vsock://@:5555")
23
24while True:
25 # クライアントからの次のリクエストを待ちます
26 message = socket.recv()
27 print(f"Received request: {message}")
28
29 # "作業"を実行します
30 time.sleep(1)
31
32 # クライアントに応答を送信します
33 socket.send(b"World")
34EOF
35
36cat <<EOF > req.py
37#
38# PythonでのHello Worldクライアント
39# vsock://@:5555にREQソケットを接続します
40# "Hello"をサーバーに送信し、"World"の応答を期待します
41#
42import zmq
43
44context = zmq.Context()
45
46# サーバーと通信するためのソケット
47print("Connecting to hello world server…")
48socket = context.socket(zmq.REQ)
49socket.connect("vsock://@:5555")
50
51# 5回リクエストを送信し、毎回応答を待ちます
52for request in range(5):
53 print(f"Sending request {request} …")
54 socket.send(b"Hello")
55
56 # 応答を取得します。
57 message = socket.recv()
58 print(f"Received reply {request} [ {message} ]")
59EOF
60
61venv/bin/python3 rep.py &
62[1] 88069
63
646566venv/bin/python3 req.py
67Connecting to hello world server…
68Sending request 0 …
69Received request: b'Hello'
70Received reply 0 [ b'World' ]
71Sending request 1 …
72Received request: b'Hello'
73Received reply 1 [ b'World' ]
74Sending request 2 …
75Received request: b'Hello'
76Received reply 2 [ b'World' ]
77Sending request 3 …
78Received request: b'Hello'
79Received reply 3 [ b'World' ]
80Sending request 4 …
81Received request: b'Hello'
82Received reply 4 [ b'World' ]
Curveとasyncio
Hello Worldは素晴らしいですが、本番環境ではおそらくasyncioとcurveを使用するでしょう。以下の例は、pyzmqのhttps://github.com/zeromq/pyzmq/blob/main/examples/security/asyncio-ironhouse.pyから取られています。
Curveキーを生成するスクリプト
1#!/usr/bin/env python
2
3import json
4
5import zmq
6import zmq.auth
7
8keys_file = "keys.json"
9
10client_pub, client_priv = zmq.curve_keypair()
11client2_pub, client2_priv = zmq.curve_keypair()
12server_pub, server_priv = zmq.curve_keypair()
13
14with open(keys_file, "w") as f:
15 json.dump(
16 {
17 "client": [client_pub.decode(), client_priv.decode()],
18 "client2": [client2_pub.decode(), client2_priv.decode()],
19 "server": [server_pub.decode(), server_priv.decode()],
20 },
21 f,
22 sort_keys=True,
23 indent=4,
24 )
REPサーバー
1#!/usr/bin/env python
2
3import asyncio
4import json
5import logging
6
7import zmq
8import zmq.auth
9from zmq.asyncio import Context
10from zmq.auth.asyncio import AsyncioAuthenticator
11
12LOGGER = logging.getLogger(__name__)
13
14
15async def run(keys: dict) -> None:
16 ctx = Context.instance()
17
18 # このコンテキストの認証局を開始します。
19 auth = AsyncioAuthenticator(ctx)
20 auth.start()
21 auth.certs["*"].update({keys["client"][0].encode(): True})
22
23 server = ctx.socket(zmq.REP)
24
25 server.curve_publickey = zmq.utils.z85.decode(keys['server'][0])
26 server.curve_secretkey = zmq.utils.z85.decode(keys['server'][1])
27 server.curve_server = True # バインド前に指定する必要があります
28 server.bind('vsock://@:9000')
29
30 msg = await server.recv()
31 LOGGER.info(f"Received {msg!r}")
32 if msg == b"Hello":
33 LOGGER.info("Ironhouse test OK")
34 await server.send(b"World")
35
36 # ソケットを閉じます
37 server.close()
38 auth.stop()
39
40
41if __name__ == '__main__':
42 if not zmq.has("vsock") or not zmq.has("curve"):
43 raise RuntimeError(
44 f"Security is not supported in libzmq version < 4.0. libzmq version {zmq.zmq_version()}"
45 )
46
47 level = logging.DEBUG
48
49 logging.basicConfig(level=level, format="[%(levelname)s] %(message)s")
50
51 with open("keys.json") as f:
52 keys = json.load(f)
53
54 asyncio.run(run(keys))
REQクライアント
1#!/usr/bin/env python
2
3import asyncio
4import json
5import loggin