HN 日本語サマリー

← 一覧へ戻る
プログラミング

LibzmqでVsockを使用する

Use Vsock with Libzmq (blog.remijouan.net)

17 pointsby remijouannet1 コメント

要約

この記事は、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