こちらは aptpod Advent Calendar 2024 12月10日の記事になります。
本日はVPoPの岩田が担当します。
今回は、弊社製品のちょっとへんな使い方ということで、リモートデスクトップ用ソフトウェアのひとつである VNC がやり取りする通信を、VM2M Data Visualizer で可視化してみようと思います。
- そもそもVNC(Virtual Network Computing)とは?
- なぜこんなことをしようと思ったのか?
- 本記事でつくるもののアーキテクチャー
- 先にできたものを動画で紹介
- RFBプロトコルの仕様
- RFBプロトコルからの画面キャプチャー画像の構築
- intdash へのデータの転送
- 今後の展望とあとがき
そもそもVNC(Virtual Network Computing)とは?
弊社のテックブログを読んでくださるような方々であればもちろんご存知だと思いますが、 VNC(Virtual Network Computing) とは、ネットワーク上の離れた場所にあるコンピュータを遠隔操作するための、いわゆるリモートデスクトップ用のソフトウェアです。詳細は Wikipedia に譲りますが、最初に Olivetti & Oracle Research Lab によって開発され、これがGPLライセンスで公開されたために、現在ではいろいろな派生ソフトウェアが存在する、という状況のようです。
私自身もあまり正しく理解できていなかったのですが、VNCというのはあくまでソフトウェアの名称であって、VNCが通信に使用しているプロトコルは RFBプロトコル(Remote Framebuffer Protocol) という名称で、別途定義されているそうです。このRFBプロトコルは、RFB 3.8 というバージョンが2011年に RFC 6143 として標準化されており、仕様は誰でも閲覧することができます。
本記事のテーマは、このVNCの通信で送受信される以下のようなデータをうまく中継して、弊社製品である VM2M Data Visualizer のダッシュボード上で可視化してやろう、というものになります。
- ディスプレイ画面の映像
- マウス・キーボード操作
- クリップボード情報
なぜこんなことをしようと思ったのか?
若干ふざけたタイトルなのでお遊び記事かと思いきや、一応このテーマにはきちんとした狙いがあり、実は技術調査のアウトプットだったりします。
弊社は、VM2M Data Visualizer というリアルタイム描画が得意な可視化ダッシュボードツールを開発・提供しています。この VM2M Data Visualizer は、ノーコードで自由にダッシュボードを組み上げられる可視化ツールで、映像からセンサーデータまで様々なデータをひとつのダッシュボードにまとめて可視化することができます。
VM2M Data Visualizer がありとあらゆるデータを一つのダッシュボードにまとめられるおかげで、ご利用いただいているお客様から 「あれもこれも、なんでもかんでも、 VM2M Data Visualizer のダッシュボードにまとめて扱いたい」 というご要望をいただくことがあります。
よくあるセンサーデータや映像・音声などであれば既存機能のみで十分対応が可能ですが、使い込んでいけばいくほどまとめたい対象も広がっていき、最終的には 「遠隔地に配置したPC上で動いているレガシーソフトウェアのUIも、まとめてダッシュボードで確認できないか」 という相談をいただいたりする機会も出てくるようになりました。
PCの画面をダッシュボードに表示するだけであれば、画面をキャプチャーして中継することもできますが、PCで稼働することを前提としたソフトウェアは、多くの場合マウスやキーボードによる操作を想定した作りになっており、画面を転送して表示しただけでは使い物になりません(可視化だけでなく、やはり操作を行いたくなります)。
もちろん、画面キャプチャーだけでなくマウスやキーボードの操作情報を中継する機能を自前実装することもできなくはありませんが、いろいろと実現方法を考えていくうちに、「もはやこれはリモートデスクトップ用のプロトコルそのものなのではないか?」という考えに思い至るようになりました。
リモートデスクトップ用のプロトコルであれば、Wiindows 向けの超有名どころである RDP や今回対象とした VNC など、すでに広く使われていて実績のあるものが存在します。そういった既存の技術を上手く活用すれば、わざわざ自前実装して車輪の再発明をする必要はありません。
これらをどうにか活用できないかと深堀って調べていくなかで、前述したとおりVNCで使用されているRFBプロトコルが標準化されていることを知り、さらにそのプロトコル仕様が極めてシンプルで簡単に扱えそうなことが分かってきたので、これを調査して製品化に向けた検討をしてみよう、と思い本記事の企画が生まれました。
本記事でつくるもののアーキテクチャー
今回の目的は、最終製品の実装ではなく、RFBプロトコルのフォーマットやシーケンスを理解することに限定し、なるべくライトに対応します。
検証作業を単純化するために、VNCサーバーの前段にTCPプロキシーを挟み込み、そのプロキシーでRFBプロトコルのやり取りをスニッフィングして intdash に引っ張り出すことで、VM2M Data Visualizer までデータを送り届けることにしました。
※ intdash は VM2M Data Visualizer がリアルタイムデータの通信のために使用する、バックエンドの通信ミドルウェアです。
ただしこれは最終的な構成ではなく、実際のプロダクトに機能を取り込むときには、もしかするとRFBプロトコルを intdash で中継できるようにするかもしれませんし、 VM2M Data Visualizer がVNCクライアントとして振る舞えるようにするかもしれません。
先にできたものを動画で紹介
Docker Compose でVNCサーバーにする Ubuntu Desktop 環境と、今回実装したプロキシーを立ち上げます。VNCサーバーには 以下の Docker イメージを使わせてもらいました。
VNCサーバーとプロキシーが起動したら、プロキシーが待ち受けているポートに向かって、VNCクライアントでアクセスしてみます。今回の動画では VNC Viewer を使用しました。
プロキシーは、VNCクライアントからのデータをそのままVNCサーバーに中継するとともに、 VM2M Data Visualizer で可視化したい情報については intdash に分岐送信します。
プロキシーが送信しているデータを可視化するように VM2M Data Visualizer のダッシュボードを構成して再生をすると、みごとにVNCサーバーとVNCクライアントが送り合っている情報を VM2M Data Visualizer のダッシュボード上に表示することができました!
RFBプロトコルの仕様
前述の通り、RFBプロトコルは RFC 6143 として標準化されており、誰でも仕様を確認することができます。
ざっとプロトコルの流れを確認すると 7.1 Handshake Messages に記載されたハンドシェイクプロセスによって、サーバーとクライアントの間でコネクションが確立されます。その後、 7.3 Initialization Messages に記載された初期化メッセージによって、サーバークライアント間で相互に通知が必要な情報をやり取りします。その後は、基本的には一方通行の通信のみが行われ、その際に使用されるメッセージが 7.5 Client-to-Server Messages および 7.6 Server-to-Client Messages です。
唯一複雑なところといえば、 7.7 Encodings で規定されている、画面イメージを送信する際のエンコーディングの仕様ですが、後述するように最も単純な Raw Encoding に限定して使用すれば、 TRLE や ZRLE などの高度なエンコーディングの仕様は一旦理解を先送りすることができます。
ハンドシェイクプロセス
ハンドシェイクは、主に プロトコルバージョン と セキュリティ方式 をサーバーとクライアントで確認しあいます。
プロトコルバージョン のハンドシェイク
プロトコルバージョンのハンドシェイクは極めて単純で、 RFB xxx.yyy\n
という12文字をサーバーから1回、クライアントから1回の1往復分送り合うだけです。今回はプロキシーを作るだけなので、12バイト分パススルーするだけでやり過ごします。
セキュリティ方式 のハンドシェイク
プロトコルバージョンのハンドシェイクが完了したら、次はセキュリティ方式のハンドシェイクを行います。ここではまずサーバーがサポート可能なセキュリティタイプのリストを送信し、クライアントが実際に使用するセキュリティタイプを選択して返信します。
VNCがもともと暗号化を想定しない単純なプロトコルであったこともあり、RFCで規定されているセキュリティ方式は、「None」または「VNC Authentication」の2方式のみようです。今回の検証においては、「None」のみ使用すれば十分であったため、「VNC Authentication」の説明は割愛します。なお、弊社製品に取り込む際には、VNCのデータはセキュアな intdash のプロトコルに格納されて伝送されることになるため、VNCやRFBプロトコルのレベルで高度な認証や暗号化に対応していなくても全く問題ありません。
クライアントが送付したセキュリティタイプをサーバーが受け入れ可能な場合は、結果を応答してハンドシェイクは完了です。受け入れられない場合は、結果や理由を応答した後にサーバーからコネクションを切断します。
初期化プロセス
初期化プロセスでは、サーバーとクライアントが相互に通知すべき情報を、それぞれ1メッセージの送信で通知しあいます。 クライアントから送信されるメッセージを ClientInit、サーバーから送信されるメッセージを ServerInit といいます。
クライアントからの初期化メッセージ(ClientInit)
ClientInit メッセージによって送信される情報は、 shared-flag のみです。本記事においてはあまり重要な情報ではないため、説明は割愛します。
サーバーからの初期化メッセージ(ServerInit)
ServerInit メッセージによって送信される情報は、以下のとおりです。
- 画面サイズ(framebuffer-width, framebuffer-height)
- 各ピクセルの表現方法(PIXEL_FORMAT)
- 画面の名称(デスクトップに関連付けられた名前)
ここで通知された画面サイズが画面全体のサイズになります。
また、PIXEL_FORMATでは1ピクセルを何ビットで表すか、RGBは何ビットずつどの順で配置されているかなどの情報を含みます。細かな仕様はRFCに譲りますが、この情報により、サーバーから送られる画面データをどの様にパースしてイメージに再構成すればよいかを知ることができます。
初期化後の通信
クライアントからサーバーへ送るメッセージ
クライアントからサーバーに送付するメッセージには、以下のものがあります。
- SetEncodings
- SetPixelFormat
- KeyEvent
- PointerEvent
- ClientCutText
- FramebufferUpdateRequest
SetEncodings を用いて、クライアントがサポート可能なエンコーディングを指定することで、サーバーに対して使用するエンコーディングを制限させることができます。ちなみに今回の実装では、最も単純な Raw Encoding のみにエンコーディングを制限させるため、クライアントからの SetEncodings メッセージを中継する際に、クライアントに内緒でこっそり Raw Encoding 以外のエンコーディングをオミットする処理を入れています。プロトコルのシーケンスとしては SetEncodings に対してサーバーからの応答確認などもなくクライアントからメッセージを送りつけっぱなしにするだけなので、このような内緒の通信内容の書き換えもできててしまいます。
SetPixelFormat は、途中でピクセルの表現方法を変更するときに使用します。
KeyEvent、PointerEvent はその名の通り、キーボードの押下やマウスの移動を通知するメッセージです。こちらのメッセージをパースすることで、どのキーが押されたか、マウスがどこに動いたかをトラッキングできます。
ClientCutText はクライアント側のクリップボードの状態をサーバーに通知します。クライアント側がクリップボードに何かを載せたとき、それがサーバーに通知されてサーバー側で利用可能になります。
FramebufferUpdateRequest は、画面キャプチャー画像を送信するよう要求するメッセージです。VNCでは基本的に、クライアントからの要求に応じて画面キャプチャー画像が返信されるプロトコルとなっています。ただし、サーバーは要求された範囲に対して画面の更新があるまで応答を先延ばしにすることができ、また、複数の要求に対して一つの応答を返せばよい、という決まりになっており、必要に応じて必要なだけ変更情報を送信できる効率のよい仕組みになっています。
サーバーからクライアントへ送るメッセージ
サーバーからクライアントに送付するメッセージには、以下のものがあります。
- SetColorMapEntries
- Bell
- FramebufferUpdate
- ServerCutText
SetColorMapEntries および Bell については、本記事においては重要なメッセージではないため、説明を割愛します。
ServerCutText は、クライアント側の ClientCutText と同様に、サーバー側のクリップボードの状態をクライアントに通知します。
FramebufferUpdate は、サーバー側で画面に更新のあった特定範囲の画面キャプチャー画像を送信します。メッセージには複数の範囲のキャプチャー画像が含まれ rectangle と呼ばれます。rectangle は位置(x, y)、サイズ(w, h)とエンコーディングのタイプ、rectangle 内のキャプチャー画像データで構成されます。画像データはエンコーディングタイプそれぞれ応じた形式で格納されますが、前述の通り今回はクライアントからの SetEncodings メッセージに細工をして Raw Encoding でしかデータが返ってこないようにしているため、 Raw Encoding として読み出せば事足ります。
RFBプロトコルからの画面キャプチャー画像の構築
Raw Encoding における画像のエンコーディング方式
Raw Encoding においては、1ピクセル分のデータサイズは ServerInit メッセージや SetPixelFormat メッセージによって送られる PIXEL_FORMAT によって決められます。
今回使用したVNCサーバーでは、32ビット = 4バイトを使用することになっていました( bits-per-pixel = 32
)。ただし、実際にRGBデータが格納されるのは24ビット = 3バイトのみで、1バイト分はパディングのようです( depth = 24
)。また、RGBのアサインについても若干注意が必要で、RGBの順ではなくBGRの順番で並んでいるようでした( red-shift=16, green-shift=8, blue-shift=0
)。
このように1ピクセルあたり4バイトとして、これを画面の左から右へ横1行分並べて、さらにその各行分のバイト列を上から下の順番になるように結合したものが、イメージデータとなります。
RFBプロトコルにおける画面データの送信方式
RFBプロトコルにおける画面キャプチャー画像のデータは、前述の通り FramebufferUpdate メッセージに含まれる複数の rectangle によって送信されます。各 rectangle はその位置(x, y)、サイズ(w, h)とエンコーディングのタイプ、rectangle 内のキャプチャー画像データを持ちます。
このように、RFBプロトコルでは画面全体のキャプチャー画像を毎回送信するのではなく、画面全体のうちの更新のあった特定の範囲の画像のみをオンデマンドで送信するという仕様を採用することで、効率良く画面の更新を伝えるプロトコルになっているようです。
今回のように、画面全体のキャプチャー画像を構成しようとする場合には、各 rectangle 毎に画像を生成するだけでなく、それまでに受け取った rectangle を保持して、画面全体を表す画像に適宜パッチをあてていく処理を実装しなければなりません。
ちなみに、画面全体の大きさは、 ServerInit メッセージに格納された画面サイズで知ることが出来ます。
Pythonによる実際のコードの紹介
以上を踏まえると、受け取った rectangle を Python で画像化する処理は次のようになります。なお、以降のコードは PIL ライブラリの Image オブジェクトへの変換までの処理になります。
ServerInit メッセージを受け取ったときの処理
ServerInit メッセージによって画面全体のサイズ (framebuffer_width, framebuffer_height)
が分かったら、画面キャプチャー全体を格納するための Image オブジェクト display_img
を初期化します。
display_img = Image.new("RGB", (framebuffer_width, framebuffer_height))
FramebufferUpdate メッセージを受け取ったときの処理
rectangle から画像データ rect_data
、位置 (x, y)
、サイズ (w, h)
を取得し、rectangle に相当する Image オブジェクト rect_img
を生成したのちに、画面キャプチャー全体を格納している Image オブジェクト display_img
の該当箇所に貼り付けます。
# イメージデータをNumPy配列化し、(h, w, 4) の3次元配列に整形する arr = np.frombuffer(rect_data, dtype=np.uint8).reshape(h, w, 4) # RGBの順序に気をつけて、Imageオブジェクトに変換する rect_img = Image.fromarray(arr[:, :, [2,1,0]]) # 全体画面に対して、更新箇所を貼り付ける display_img.paste(rect_img, (x, y))
あとは、 Image オブジェクトを JPEG にするなり PNG にするなり好きな方法で画像ファイル化すれば、画面キャプチャー画像をファイルとして取得出来ます。
intdash へのデータの転送
RFBプロトコルをパースして画面キャプチャーデータを取得できるようになったら、あとはそれを intdash に送信すれば VM2M Data Visualizer で受信してダッシュボードに表示できるようになります。今回はプロキシーを Python で実装したので、Python 用の intdash のクライアントライブラリ iscp-py を使用しました。iscp-py には、intdash とのリアルタイム接続のためのインターフェイスが一通り実装されており、intdashへのデータ送信プログラムを簡単に書くことができます。
今回は、この iscp-py を使って、プロキシー内の以下の処理ポイントに intdash へのデータ送信を加えてみました。
- FramebufferUpdate の受信時(画面キャプチャー)
- KeyEvent の受信時(キーボード操作)
- PointerEvent の受信時(マウス操作)
- ClientCutText, ServerCutText の受信時(クリップボード)
KeyEvent、PointerEvent メッセージはJSON形式のテキストで、ClientCutText、ServerCutText メッセージはそのままの文字列で、FramebufferUpdate メッセージは JPEG 形式の画像として送信することで、 VM2M Data Visualizer のダッシュボード上で、標準パーツを使用して可視化できるようになります。
今後の展望とあとがき
今回のお試し実装により、RFBプロトコルの概要が掴めました、RFCには高度なエンコーディングも記載されているものの、クライアントからの要求によりサーバーに単純なエンコーディングを強制することが可能であることも分かりました。また、一部のエンコーディングを除けば、全体として極めて単純なプロトコルであることが改めて確認できました。
ここまで単純明快なプロトコルであれば、例えば VM2M Data Visualizer のパーツに、VNCクライアントとして振る舞うものを追加することもさほど難しくはなさそうです(もちろん、実際にプロダクト化を進めるにあたっては、 TRLE や ZRLE などのエンコーディングも必要に応じて調査をし、対応を検討します)。
様々なお客様とのプロジェクトを進めていくなかで、実はDX化を進める現場にも、まだまだレガシーなシステムがたくさん稼働していることは把握しています。弊社の VM2M Data Visualizer は、そういったレガシーシステムとも連携ができる柔軟なプロダクトになれれば良いなと、今回の調査以外にも日々色々と検討を進めています。
今後も、皆さまのご意見を取り入れつつ、より良いプロダクトを作っていきたいと考えておりますので、すでに利用してくださっている皆さま、これから使ってみたいとご興味を持っていただけた皆さま、ぜひ気軽にお声がけいただき、弊社のサービスやプロダクトに対するご意見をいただければ幸いです。
本記事で取り上げたレガシーシステムとのVNCによる連携につきましても、もし活用イメージをお持ちいただけましたら、ぜひご意見をお寄せください。お問い合わせはこちらです。
最後までお読みいただきありがとうございました。