自宅サーバのデスクトップへブラウザからアクセスできるように「noVNC」を設える


主な目的としては、以下のようで、

* 自宅の無線 LAN ルータの設定を、外からイジりたい
* Dropbox や ownCloud のクライアントのように、サーバ上で動いてはいるけれど GUI の方が操作しやすいアプリへ、外からアクセスしたい（SSH だとしんどい）
* スマホから自宅のブラウザへアクセスして艦これの遠征を出s（以下自粛）

その際の要件なんですが、

* 経路を暗号化したいが、都度 SSH 等でトンネルするのは面倒だ
* VNC クライアントをクライアントごとに用意するのも面倒だ
* ブラウザだけで何とかならない？ HTTPS のサーバ証明書ならば、安かったので入れてあるんだけれど

といったところです。

> ガラケーのカバー率が気になるとか、Extended Validation 証明書でなければ、というのでないかぎり、サーバ証明書なんて年で 1,000 円くらいですので皆さんもぜひ。あれ？ namecheap の鍵屋から、RapidSSL が外されていますね。値上がっちゃったからか？ まあ、COMODO でも GeoTrust でもいいんで、1,000 円くらいのやつで。 ∥ [Cheap SSL Certificates from $7.95/yr • Namecheap.com](https://www.namecheap.com/security/ssl-certificates.aspx))

# noVNC について

「noVNC by kanaka」 ∥ [noVNC](http://kanaka.github.io/noVNC/)

HTML5 で実装された VNC クライアントです。JavaScript で書かれており、描画に Canvas、通信に WebSocket を用います。ブラウザさえあれば iOS 等でも動きます。仕事で使って知ったのですが、結構なパフォーマンスでして、インターネット越しでもサクサクと動きます。

下図は最終的に、ブラウザ内にデスクトップを表示して、LAN 内から無線 LAN ルータの設定画面にアクセスしている様子。

![ss-2015-01-11-21.46.30.png](https://qiita-image-store.s3.amazonaws.com/0/30059/077b359b-1694-5ef1-f607-61a4cb615849.png "ss-2015-01-11-21.46.30.png")

# デスクトップへの VNC アクセスを許可する（Ubuntu の場合）

今回の使い方では、VNC の暗号化されていない通信は LAN 内だけで閉じるので、LAN 内での暗号化はしません。外へ出る際には、後ほど、リバースプロキシが SSL/TLS で暗号化するようにします。

まずはデスクトップ側の設定です。Ubuntu であればリモートデスクトップは Vino の VNC（RFB プロトコル）で実現されているので、設定は `vino-preferences` で行います。

Ubuntu 14.04 から RFB の暗号化が標準になったようで、Mac 標準の VNC クライアントで繋げられなくなってしまった。右記で、少しは進んだが、ネゴシエーション時に止まったきりになる。 ∥ [Remote desktop doesn't works after upgrade from 13.10 to 14.04 - Ask Ubuntu](http://askubuntu.com/questions/455754/remote-desktop-doesnt-works-after-upgrade-from-13-10-to-14-04)

VNC クライアントがどうも宜しくないので、RFB プロトコルの卸元たる RealVNC 社の無料クライアントを、ひとまずテスト用に入れて進める。 ∥ [RealVNC™ remote access & control software for desktop and mobile](https://www.realvnc.com/)

そして、暗号化を切ります。入っていなければ、dconf Editor を入れます。

~~~~console:
$ sudo aptitude install dconf-editor
~~~~

そして、dconf Editor（コマンドは `dconf-editor`）を起動し、org → gnome → desktop → remote-access と辿り。`require-encryption` を切る。

# VNC への WebSocket プロキシを起動する

最近は WebSocket な RFB を直接に喋る VNC サーバも出てきているようなのですが、ここでは、noVNC に同梱されているプロキシプログラムを実行し、プロキシさせます。

~~~~console:
$ utils/websockify 5901 localhost:5900
~~~~

上記を実行することで、デスクトップが動いているサーバ上で 5900 番ポートに出ている VNC（RFB プロトコル）を WebSocket でラップし、5901 番ポートに晒します。

同梱の `websockify` コマンドは、指定したポート（上記の場合は 5901 番ポート）を専有して通信します。パスを指定されてもガン無視します。つまり、ws://localhost:5901 だろうが ws://localhost:5901/hoge/fuga だろうが構いません。

# リバースプロキシでアップグレード HTTP (SSL) で WebSocket を喋らせる 

さて、リバースプロキシを通じて外へ出す場合に 443 番ポートを専有されてはたまりませんので、リバースプロキシ上でパスを切ります。

うちの場合は、サーバ上に nginx が動いていて、ここでサーバ証明書を用いて TLS 通信をしています。

以下のように設定しました。右記を参考に。 ∥ [NGINX as a WebSockets Proxy - NGINX](http://nginx.com/blog/websocket-nginx/)

~~~~text:
    ...

    location /proxy {
        proxy_pass http://localhost:5901;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    ...
~~~~

これで、https://～ → wss://～ のアップグレード機構が動く模様。詳細は後で調べておく。

# noVNC を設置＆カスタム

noVNC を GitHub から clone してくると、ルートに「vnc.html」と「vnc_auto.html」というサンプルがあります。

nvc.html の方は汎用のクライアントであり、起動すると、接続先のホストやポート、パスワードなどを入れる UI が使えます。

一方 vnc_auto.html の方は、他のプロジェクトに埋め込んでカスタマイズする用途で書かれており、URL のパラメータで、接続先を指定するようになっています。右記参照。 ∥ [Provide a way to specify WebSocket URI path · Issue #71 · kanaka/noVNC](https://github.com/kanaka/noVNC/issues/71)

以下のように、先ほど外出しした WebSocket のパスを指定し、`encrypted` で SSL/TLS 通信をするように指定して実行すれば、すんなりと繋がります。`password` は VNC (RFB) のパスワードです。

~~~~text:
https://server.example.com/noVNC/vnc_auto.html?host=server.example.com&port=443&encrypt=true&path=/proxy&password=********
~~~~

HTTPS の通信ですので、`vnc_auto.html` を出している側で Basic 認証等を加えると良いと思います。

# セキュリティをどうするか？

VNC のパスワードが URL に乗るのは気持ちが悪いですね、アドレスバーで丸見えです。コードの中にパスワードを入れ込んでも良いのですが、ここでは vnc_auto.html に書かれているように、クッキーを用いてみます。

（ちゃんとやるならば、クライアント証明書を `optional` で導入し、RFB プロキシのところでは証明書を必須にするのが正しいかなぁ）

今回のケースでは、HTML は Basic 認証つきの HTTPS から出ているんですから、HTML 側と RFB プロキシ側とで示し合わせて共通鍵でセキュアであれば良いわけです。

vnc_auto.html には、Cookie を用いてトークンを受け渡す仕組みがあります。

~~~~javascript:noVNC/vnc_auto.html
    ...
    // If a token variable is passed in, set the parameter in a cookie.
    // This is used by nova-novncproxy.
    token = WebUtil.getQueryVar('token', null);
    if (token) {
      WebUtil.createCookie('token', token, 1)
    }
    ...
~~~~

URL パラメータ（`token=～`）で渡すとアドレスバーに見えてしまいますので、何も解決しません。コードを書き換えます。

~~~~javascript:noVNC/vnc_auto.html
    ...
    token = WebUtil.getQueryVar('token', "hogehoge");
    ...
~~~~

WebSocket プロキシの方で、クッキーにこのトークンが含まれていなければ蹴るようにします。先の nginx の設定に、以下を足します。

~~~~text:
    ...
    location /proxy {
        if ($cookie_token != "hogehoge") {
          return 403;
          break;
        }
        ...
~~~~

とりあえずこれでいいや。

# 残課題

ドットバイドットで表示してしまうので、デスクトップの解像度が高いと、クライアント側でブラウザの画面からハミ出してしまって困る。どうやって調整すれば良いのだろう。

よって、まだ iPhone で艦これができていない。

追記（Jan 14 2015）: こんなのが出てきた。Linux ターゲット（ベータ）が Ubuntu 12.04 にしか対応していないことと、クライアント側にアプリを入れる必要があることを除けば、スキが無い。艦これ用途には、これで自宅の Windows に繋げば良いように思えてきたので、ちょっとモチベーションが下がっているところ。 ∥ [Google、iOS版 Chrome Remote Desktop 提供開始。PCへウェブ経由でリモートアクセス - Engadget Japanese](http://japanese.engadget.com/2015/01/13/google-ios-chrome-remote-desktop-pc/)