Haproxyを用いて443ポートで複数サービスをホストする

#はじめに
グローバルIPが1つしかないプライベートなサーバーで443ポートはかなり貴重です。

元々SSH,OpenVPN,HTTPSといった各サービスの標準ポートを使えば何ら問題はないんですが、80,443しか通さない強固なプロキシや◯盾等のけしからんネットワークを掻い潜るためには、どうしても443ポートを使用してHTTPSに偽装する必要が生じます。

それらのサービスを同時にホストするためのプロキシとして`SSLH`がありますが、プロトコル判定ルールがソースレベルで定義されているため、定義にないプロトコルを追加することは少々難しいです。
>
拙記事 SSLHでSoftether(SSTP)とApacheを共存
https://qiita.com/Shachihoko/items/64a519b930b26ee4e81e
>

そこで今回はDPIっぽい挙動ができる汎用プロキシである <b>Haproxy</b> を用いていくつかのプロトコルの通信を同時にホストする方法を模索します。
>参考サイト
WebサーバーとSSHサーバーが同一ポートで待ち受けるSSLHの動作原理とは
https://blog.millenary.net/archives/48
>

2020/04/24 フロントエンドサーバーの設定に1行追記しました。

#環境
今回対象とするプロトコル
・HTTPS
・SSTP
・OpenVPN
・Softether VPN
・SSH

現状443ポートをリッスンしているサービスがあれば変更しておきます
ここでは各サービスのアドレスを以下として進めます.

・Nginx(HTTPS)   127.0.0.1:10443
・sshd           127.0.0.1:22
・Softether(SSTP,OpenVPN,SoftetherVPN)   127.0.0.1:1194

※SSTPとSoftetherVPNプロトコルの判別に<b>SNIホスト名</b>を使用するので、加えて`vpn.foo.bar`というホスト名で正引きできるようにしておきます

#HAproxyの設定
インストール

```
$sudo apt-get install haproxy
```

設定

```/etc/haproxy/haproxy.cfg
global
        log /dev/log    local0
        log /dev/log    local1 notice
        chroot /var/lib/haproxy
        stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
        stats timeout 30s
        user haproxy
        group haproxy
        daemon

        # Default SSL material locations
        ca-base
        crt-base

        # Default ciphers to use on SSL-enabled listening sockets.
        # For more information, see ciphers(1SSL). This list is from:
        #  https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
        # An alternative list with additional directives can be obtained from
        #  https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=haproxy
        ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS
        ssl-default-bind-options no-sslv3

defaults
        log     global
        mode    http
        option  httplog
        option  dontlognull
        timeout connect 5000
        timeout client  50000
        timeout server  50000
        errorfile 400 /etc/haproxy/errors/400.http
        errorfile 403 /etc/haproxy/errors/403.http
        errorfile 408 /etc/haproxy/errors/408.http
        errorfile 500 /etc/haproxy/errors/500.http
        errorfile 502 /etc/haproxy/errors/502.http
        errorfile 503 /etc/haproxy/errors/503.http
        errorfile 504 /etc/haproxy/errors/504.http
```

[global]で書き換えが必要なのは以下だけです

```
# Default SSL material locations
        ca-base #SSL証明書の公開鍵
        crt-base #SSL証明書の秘密鍵
```
末尾にプロキシの設定を追記していきます。

###フロントエンドサーバーの設定

```/etc/haproxy/haproxy.cfg
frontend https
    mode                tcp #TCPプロキシとして稼働させるのでtcpを指定
    bind                *:443　#443で待受
    tcp-request         inspect-delay 5s #遅延を5秒まで許可(SSLでは必要)
```
###SSHプロトコルの判別
SSHプロトコルの最初のペイロードパケットは`SSH-2.0`の7文字で始まるため、これを16進数表記にした`5353482d322e30`で始まるという条件を <b>acl</b> 属性で設定し、これにマッチする接続を`bk-ssh-server`に転送

```
    acl                 ssh-server req.payload(0,7) -m bin 5353482d322e30
    tcp-request         content accept if ssh-server
    use_backend         bk-ssh-server if ssh-server
```

###SSTP,SoftetherVPNプロトコルの判別
SSTPプロトコルのパケットには通常のHTTPS通信と同様にSNIのためのホスト名情報が含まれているので、これをHAproxyで読み取り、事前にVPN用として設定したホスト名`vpn.foo.bar`と一致すれば`bk-sstp-server`に転送

```
    use_backend bk-sstp-server if { req_ssl_sni -i vpn.foo.bar }
```

###HTTPSプロトコルの判別
SSTPプロトコルを分岐した後に、SSLのClient Helloを通知してきた残りのパケットを`bk-nginx-server`に転送

```
    use_backend bk-nginx-server if { req.ssl_hello_type 1 }
```
###OpenVPNの判別
(WireSharkでキャプチャしても尻尾が掴めなかったので)
以上の条件にマッチしなかったパケットは全て`bk-openvpn-server`に転送

```
    default_backend     bk-ovpn-server
```

以下がフロントエンドの設定です

```/etc/haproxy/haproxy.cfg
frontend https
    mode                tcp
    bind                *:443
    tcp-request         inspect-delay 5s

    tcp-request content accept if { req.ssl_hello_type 1 } #これを明示的に記述しないと判定が安定しなかった

    acl                 ssh-server req.payload(0,7) -m bin 5353482d322e30
    tcp-request         content accept if ssh-server
    use_backend         bk-ssh-server if ssh-server

    use_backend bk-sstp-server if { req_ssl_sni -i vpn.foo.bar }
    
    use_backend bk-nginx-server if { req.ssl_hello_type 1 }
    
    default_backend     bk-ovpn-server
```

###バックエンドサーバの設定
フロントエンドの設定に続ける形で書き込む
各自の環境に合わせて設定してください、ここでは前述の仮定環境を想定しています

```/etc/haproxy/haproxy.cfg
backend bk-sstp-server
    mode                        tcp
    server                      default 127.0.0.1:1194

backend bk-ssh-server
    mode                        tcp
    server                      default 127.0.0.1:22

backend bk-nginx-server
    mode                        tcp
    server                      default 127.0.0.1:10443

backend bk-ovpn-server
    mode                        tcp
    server                      default 127.0.0.1:1194
```

###HAproxyの起動

```
$ sudo systemctl start haproxy
$ sudo systemctl enable haproxy
```

#最後に
※このプロキシを設定した状態で、SoftetherVPNクライアントから接続する場合はクライアントの設定で<b>UDP高速化機能を無効にする</b>にチェックを入れてください。

SSLHを使用するのが設定としては簡単ですが、この方法だと判別ルールさえ設定できれば独自プロトコルを含めた柔軟なTCPプロキシサーバーが構築できるので応用は効くのではないかと思います。