OPNsenseのHAProxyで443ポートを使い回す(https,ssh,OpenVPN)

HAProxyについて

HAProxyはTCPおよびHTTP向けの高機能なロードバランサー/プロキシである。 バックエンドへの振り分け条件はパケットレベルで定義できるため、条件を上手に定義できればプロトコルレベルのマルチプレクサとして振る舞うこともできる。

OPNsenseはこのHAProxyをプラグインとしてサポートしている。 そこで、様々な理由で競合しがちな443ポートを複数のプロコトルで使い回せるようにしてみる。

OPNsense設定

前提条件として、WAN、LAN、DMZの3つのゾーンが存在し、OPNsenseは3つのインタフェースでそれぞれと接続しているという環境。

f:id:nullsnet:20210918221755p:plain

System: Settings: Administration

まずはOPNsense自身が443ポートを待ち受けできるようにする。 デフォルトでは管理画面へのアクセスに使用している可能性があるので見直す。 Listen InterfacesをLANだけにし、WAN側はHAProxyで使えるようにしておく。

f:id:nullsnet:20210918220636p:plain

Services: HAProxy: Settings:

Real Servers

バックエンドとなるサーバを登録する。 今回はOpenVPNSSHHTTPSの3つを登録する。

Virtual Services: Backend Pools

OPNsenseのフロントエンドとなるサービスを定義する。 Real Serversで定義したサーバを登録していけばよい。

Virtual Services: Public Services

OPNsenseのフロントエンドとなるサービスを定義する。 今回はWAN側IPの443をリッスンする設定とする。

Rules & Checks: Conditions / Rules

いよいよプロトコルごとの振り分けを定義していく。 基本的には、TCPの3WAYハンドシェイク後の、各プロトコルの最初のパケットに対する条件(Conditions)を定義してやれば、当該コネクションが丸ごと処理対象となる。 更に、ルール(Rules)にて条件に一致したパケットをどのバックエンドに流すか定義することで、パケットの振り分けが可能となる。

SSH

Conditon

調べてみると、SSHのハンドシェイクのペイロードは「SSH-2.0」から始まるため、これのバイナリ表記である「5353482d322e30」を条件にすればよい、という情報が出てくる。 パケットをキャプチャして調べてみると、確かにそのようだ。 SYN -> SYN/ACK -> ACKの直後のパケットのペイロードが「SSH-2.0」で始まっているのが確認できる。

f:id:nullsnet:20210918231744p:plain

この条件をHAProxyに設定する。 OPNsenseのHAProxyはGUIでのACL設定ができないため、設定値のパススルーで直接設定する。 「ペイロードの0byteから7byte分がバイナリで5353482d322e30であること」としてreq.payload(0,7) -m bin 5353482d322e30を設定する。

f:id:nullsnet:20210918232114p:plain

Rule

あとは、作成したConditionsに一致するパケットをsshのバックエンドに流す、というRuleを定義すればよい。

f:id:nullsnet:20210918233946p:plain

HTTPS(TLS)

Conditon

HTTPS、というかTLSの場合は簡単で、「Client Hello」を送信してきたもの、という条件でよい。

f:id:nullsnet:20210918232547p:plain

こちらもパススルーでreq.ssl_hello_type 1を設定する。

f:id:nullsnet:20210918232617p:plain

他にも、SNIに含まれるドメイン名を指定する方法があるようだ。

Rule

こちらもSSHと同様にRuleを定義すればよい。

加えてもう一つ、先程のConditonとは別に、tcp-request content acceptのルールを作成する。 TLSを扱う場合は必要なルールのようだ。

f:id:nullsnet:20210919000234p:plain

Enhanced SSL Load Balancing with Server Name Indication (SNI) TLS Extension - HAProxy Technologies

OpenVPN

Condition

OpenVPNに関してはあまり情報がなく、default_backendをOpenVPNとすることで「HTTPSでもSSHでもなければOpenVPNへ流す」という設定例しか見つからなかった。 これは実際やってみると、443ポートへのHTTPSでもSSHでもないアクセス(おそらくbotによる攻撃の類と思われる)が全てOpenVPNに集中してしまう。 これは非常に気持ち悪いのでなんとかしたい。

sslhやOpenVPNソースコードを読んだがよくわからなかったので、力技でなんとかしてみることにした。 OpenVPNへのコネクション時のパケットを複数回キャプチャし、共通点を見つけて条件として定義する、という手法でやってみる。 この方法はOpenVPNの設定値にも依存するだろうし、他の環境でうまくいくかはわからない。

1回目

0040         00 2a 38 be 95 8e 13 12 53 5f 66 e1 aa 4a   ...*8.....S_f..J
0050   16 16 a7 eb a6 3b 27 5d 08 cc 81 88 ee cc 2c 63   .....;']......,c
0060   b4 00 00 00 01 61 45 c3 f4 00 00 00 00 00         .....aE.......

2回目

0040         00 2a 38 44 29 20 dc 73 e6 dc 19 8d 0d 2b   .J.*8D) .s.....+
0050   8b cc 75 11 47 cb aa de 00 67 e4 bc 3b 62 73 b9   ..u.G....g..;bs.
0060   dd 00 00 00 01 61 46 01 c1 00 00 00 00 00         .....aF.......

どうやらペイロード[0]~[2][35]~[38]が共通しているようだ。 ここから条件を作成する。 どの条件に従うかはルール側で定義するため、条件は2つに分けて定義する必要がある。 今回はそれぞれopenvpn_1、openvpn_2という名前で定義した。

  • 条件1(openvpn_1)
    • req.payload(0,3) -m bin 002a38
  • 条件2(openvpn_2)
    • req.payload(31,4) -m bin 00000001

この条件で本当に正しいかはわからないので、様子見して調整することにする。

Rule

Rule側ではopenvpn_1とopenvpn_2の両方が成立する場合としたいため、ANDを設定する。 これでOpenVPNのパケットが振り分けられる。

f:id:nullsnet:20210918234744p:plain

Virtual Services: Public Services

最後にOPNsenseがリッスンするサービスを定義する。 Listen AddressesにWAN側のIPアドレス:443を設定し、Select Rulesに定義したルールを設定すればよい。

また、Option pass-throughにはtcp-request inspect-delay 5sを設定する必要があるようだ。 Introduction to HAProxy Stick Tablesにそう書いてあったし、確かに書かないとうまく動かなかった。

You only need to use this in a frontend or backend when you have an ACL on a statement that would be processed in an earlier phase than HAProxy would normally have the information. For example, tcp-request content reject if { path_beg /foo } needs a tcp-request inspect-delay because HAProxy won’t wait in the TCP phase for the HTTP URL path data. In contrast http-request deny if { path_beg /foo } doesn’t need an tcp-request inspect-delay line because HAProxy won’t process http-request rules until it has an HTTP request.

f:id:nullsnet:20210918235359p:plain

動作確認

HAProxyのLog Fileから確認できる。 ちゃんと443で3つのプロトコルが振り分けられているようだ。

2021-09-19T00:08:36 haproxy[16827] XXX.XXX.XXX.XXX:11395 [19/Sep/2021:00:08:36.881] outbound_443 tls/tls 8/0/44 5804 -- 2/2/1/1/0 0/0
2021-09-19T00:09:40 haproxy[16827] XXX.XXX.XXX.XXX:5243 [19/Sep/2021:00:09:01.365] outbound_443 ssh/ssh 1/0/39350 2953 cD 2/2/0/0/0 0/0
2021-09-19T00:17:08 haproxy[56327] XXX.XXX.XXX.XXX:23787 [19/Sep/2021:00:17:01.754] outbound_443 openvpn/openvpn 1/0/6979 14882 -- 2/2/0/0/0 0/0    

コンテナでGPU acceralatedなWebGLを動かしてブラウザゲームを遊ぶ

モチベーション

とあるブラウザゲームをプレイしています。 このゲームは、1日の回数制限があるが報酬がおいしいコンテンツ、いわゆる日課がたくさんあります。 毎日やるのは正直面倒です。

なので、基本的に「フルオート」機能で消化します。 これはスキル発動や攻撃行動を自動で行う機能で、オンにすれば自動で敵を倒してくれるので便利です。 ただし、操作を自動化するだけで戦闘自体はリアルタイムで進行するため、画面はアクティブにしておく必要があります。 スマホで「フルオートだけオンにして放置する、別の作業をする」といったことは出来ないので、少々面倒です。

そこで、簡単にフルオート放置ができる環境を作ってみます。

  • PC/スマホから操作できること
  • ブラウザだけで操作できること
  • 利用規約に則ること

実現方法

  1. DockerとNVIDIA Container ToolkitでX11OpenGL(WebGL)が動くコンテナを作る
  2. GoogleChromeを動作させる
  3. noVNCでブラウザから操作可能にする

少し調べてみると「ホスト側のtmp/.X11-unix$DISPLAYをコンテナへ共有し、描画はホスト側で行う」という手法が多く散見されます。 ホストを汚したくない&ヘッドレスにしたいので、コンテナ側に全て閉じ込める方式にします。 具体的には、X.Org Serverもコンテナ内で実行します。

諸元

とても古い機材ですが、とりあえずNVIDIA Container Toolkitに対応してさえいればなんでもいいはずです。

CPU Core i5-3230M
Host OS Ubuntu 20.04
Container base Ubuntu 20.04
Docker version 20.10.8
GPU NVIDIA GeForce 650M
GPU driver version 470.57.02
CUDA version 11.4

構築

ホスト

NVIDIA関連のドライバやミドルウェアをインストールします。 以前は依存関係が複雑で面倒だったような気がしますが、現在は非常に簡単になったようです。

ドライバ

リポジトリを追加してcuda-driversをインストールすれば最新版が入ります。 ここの通りやるだけです。

ミドルウェア

こちらもリポジトリを追加してnvidia-docker2だけインストールすれば、container-toolkitなども同時にインストールされます。 ここの通りやるだけです。

nvidia-smiコマンドが実行できればOK。 次の作業のために、実際にインストールされたドライバのバージョンを確認しておきます。

$ nvidia-smi
Sun Aug 15 06:02:11 2021
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 470.57.02    Driver Version: 470.57.02    CUDA Version: 11.4     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  NVIDIA GeForce ...  On   | 00000000:01:00.0 N/A |                  N/A |
| N/A   56C    P8    N/A /  N/A |     79MiB /  2000MiB |     N/A      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

コンテナ

NVIDIAがコンテナをいくつか公開していますが、これらにはX11に必要なファイルを含んでいないようです。 そもそも現状ではX11をサポートしていません。

Display (e.g. X11, Wayland) is not officially supported.

なので自分で環境構築する必要があります。コードはこちら。

ホストとコンテナのドライバのバージョンを合わせる必要があるので、ビルド時の引数で指定します。

docker-compose build --build-arg DRIVER_VERSION=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader)
docker-compose up -d

:8081/vnc.htmlにアクセスすればnoVNCの画面が出ます。 自動的にOpenboxとターミナルが起動するようにしてあります。

f:id:nullsnet:20210815184433p:plain

ここからChromeを起動する場合、rootなのでgoogle-chrome --no-sandboxとする必要があります。

できたもの

コンテナ上で動作するGPU acceralatedなWebGLの環境ができました。 ブラウザを閉じても問題なくフルオートできています。 速度も問題なさそうです。 HTML5に対応してさえいれば、どんな端末からでも操作可能です。 これでフルオート放置が捗ります。

ちゃんとした(?)用途としては

とかがあるでしょうか。あんまりなさそうですが。