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