5 min read

Rust で TLS Proxy を作ってる

注: まぁまぁ長いです。

自社製品では nginx を TLS 終端 Proxy として推奨しています。Rust で Sans I/O な HTTP/1.1WebSocket ライブラリを作ったこともあり、この部分を自作の TLS Proxy に置き換えるチャレンジをしています。もともと Varnish で使われている Hitch を作ってみたいなと思っていました。

結果

自社製品の前段に Nginx の代わりにおいて普通に動くようになりました。ただ性能はまだまだ課題ありです。Nginx 本当に凄いです。

最小

まず非同期ライブラリは Tokio 、TLS ライブラリは Rutls (tokio-rustls) 、暗号ライブラリは Rustls が推奨している aws-lc-rs を採用しました。あとは依存は最小限に抑えることにしました。依存ライブラが増えると覚えることが増えるので ... 。

最新の nginx で既に実装されている ACME (TLS-ALPN-01) も使ってみたかったので、ACME クライアントを実装することにしました。IP アドレス証明書が使いたかったので ... 。

ちなみに時雨堂は Let's Encrypt のスポンサーです。

実装

最低限これだけあればいいだろうという機能を実装しました。HTTP/1.1 とWebSocket ライブラリも PBTFuzzing をがっつり当ててセキュリティ対策はできるだけやっています。

サーバー

  • IPv4 / IPv6 デュアルスタック対応
  • 複数アドレスで同時リッスン

TLS

  • TLS 終端
  • マニュアルモード
    • Certbot などを利用して証明書を取得している場合
  • ACME モード
    • 起動するだけで自動で証明書を取得
    • 自動証明書取得
    • TLS-ALPN-01 チャレンジ
    • DNS 名および IP アドレス証明書に対応
  • TLS バージョン制限
    • TLSv1.2 / TLSv1.3
  • セッションキャッシュ
    • チケットの有効化・無効化
  • ハンドシェイクタイムアウト設定
  • ALPN による HTTP/1.1 プロトコルネゴシエーション

ルーティング

SNI を使ったルーティングをよく使うので nginx よりも使いやすく実装しました。

  • SNI プリリード
  • SNI ベースルーティング
  • transparent モード
    • TLS 終端なし
    • そのまま転送
  • tls_terminate モード
    • TLS 終端後
    • TCP または HTTP で転送
  • reject モード
    • 接続拒否、サイレントディスカード
    • 今の時代はアタックが訳わからんくらい来るので対策
    • 特に Let's Encrypt で証明書を取ると間違いなく来る

HTTP プロキシ

メインなので一応しっかりと。ただ自社製品とだけ繋がればいいので最低限でもある。

  • HTTP/1.1 プロキシ
    • HTTP / HTTPS upstream 対応
    • HTTP/1.1 のみ
    • HTTP/{2,3} は非対応で対応予定もなし
  • WebSocket プロキシ
    • HTTP/1.1 Upgrade、HTTP upstream のみ
      • WSS の upstream 接続は一旦なし
      • TLS 終端のみ
    • HTTP/{2,3} は非対応で対応予定もなし
  • パスマッチング
    • nginx の Location を最低限模倣
    • 完全一致 / 前方一致 / 正規表現
  • Host ヘッダー保持(preserve_host
    • 手抜き
  • 任意ヘッダー転送(forward_headers
    • 一応リストで指定出来る
  • 自動ヘッダー追加(X-Real-IP, X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host, Via, Date
    • よく使うのは勝手に付けることにした
  • upstream タイムアウト設定
  • upstream TLS 接続設定(SNI 送信制御、セッション再利用)
    • この辺りは一応しっかり作ってみた
  • Expect: 100-continue 対応
  • 接続プール(Keep-Alive 接続の再利用)
  • エラーレスポンス
    • 404 Not Found
    • 499 Client Closed Request
      • nginx のマネ、このステータスコードが結構好き
    • 502 Bad Gateway
    • 504 Gateway Timeout
    • 505 HTTP Version Not Supported
      • HTTP/1.1 のみなので、まさかこれを実装することになるとは

レート制限

セキュリティ対策で最低限実装。

  • リクエストレート制限
    • Token Bucket アルゴリズム
  • 同時接続数制限
  • ルートごとの制限設定
  • カスタマイズ可能なエラーステータスコード

PROXY PROTOCOL

v1 以外あまり使い道をしらないのでとりあえず v1 だけ

  • PROXY PROTOCOL v1 対応
    • IPv4 / IPv6

UI

もともと作りたかった組み込み UI 方式。認証機能ありで。気軽に細かい状態がブラウザで確認できる。

  • 専用ポートでの UI サーバー起動
  • JSON-RPC 2.0 API(/rpc エンドポイント)
    • version - バージョン確認
    • stats.connections - 接続統計
    • stats.tls - TLS 統計
  • 外部 UI サイトへのプロキシ

TLS Proxy メトリクス

NGINX Prometheus Exporter を使っているが、別に OpenMetrics は難しいプロトコルでもないので最初から TLS Proxy が持ってるのも悪くないだろうということで実装しました。統計情報は最低限で少なめです。

  • OpenMetrics(Prometheus)形式
  • /metrics エンドポイント
  • 収集メトリクス:
    • connections_total - 総接続数(ラベル: route_type
    • connections_active - アクティブ接続数
    • requests_total - HTTP リクエスト総数(ラベル: status
    • errors_total - エラー総数(ラベル: error_type
    • upstream_timeout_total - upstream タイムアウト数
    • rate_limit_req_rejected_total - レート制限による拒否数
    • rate_limit_conn_rejected_total - 接続数制限による拒否数

システムメトリクス

TLS Proxy 専用以外に Node Exporter 的な仕組みも追加しました。libc 使えばサクサクっと実現できて本当に便利です。

メトリクス 説明
cpu_seconds_total CPU モード別使用時間(ラベル: cpu, mode
memory_*_bytes メモリ情報(total / free / available / buffers / cached / swap_total / swap_free)
load1 / load5 / load15 ロードアベレージ
filesystem_*_bytes ファイルシステム情報(ラベル: mountpoint, fstype
disk_*_total ディスク I/O(ラベル: device
network_*_total ネットワーク統計(ラベル: device
filefd_allocated / filefd_maximum システム全体の FD 使用状況
process_* プロセス情報(CPU 時間、メモリ、FD、スレッド数)
tcp_connections TCP 接続状態別カウント(ラベル: state
boot_time_seconds システム起動時刻
time_seconds 現在時刻
info システム情報(ラベル: version, hostname, os, os_version, kernel

設定例

設定ファイルは JSONC を採用しました。コメントが書けて、末尾カンマが書ければまぁ不満はないだろうということでの採用です。まだ環境変数での指定は対応できていないので、そのうち対応したいなと思っています。気軽に使えるのを目指していきたいところです。

以下は証明書は自前で取ったバージョンの設定ファイルです。あくまで自社製品向けですので、癖がありますのでご注意を。

{
  "proxy": {
    "listen": ["0.0.0.0:443", "[::]:443"],

    "tls": {
      "mode": "manual",
      "certificate": "/path/to/fullchain.pem",
      "private_key": "/path/to/privkey.pem",
      "handshake_timeout_secs": 10,
      "min_protocol_version": "1.2",
      "max_protocol_version": "1.3",
      "session_cache": false,
      "session_tickets": false,

      "routing": {
        "default": {
          "mode": "transparent",
          "upstream": "192.0.2.1:3478"
        },
        "sni": [
          {
            "server_name": "sora-turn.example.com",
            "mode": "tls_terminate",
            "upstream": "192.0.2.1:3478"
          },
          {
            "server_name": "sora.example.com",
            "mode": "tls_terminate",
            "handler": "http"
          }
        ]
      }
    },

    "http": {
      "upstream_timeout_secs": 60,
      "server_header": true,
      "routes": [
        {
          "path": "/signaling",
          "type": "websocket",
          "upstream": "http://127.0.0.1:5000/signaling"
        },
        {
          "path_prefix": "/api",
          "upstream": "http://127.0.0.1:3000/"
        },
        {
          "path_regex": "^/(whip|whip-session|whep|whep-session)/",
          "upstream": "http://127.0.0.1:5000",
          "preserve_host": true,
          "forward_headers": ["Authorization"]
        },
        {
          "path": "/.ok",
          "upstream": "http://127.0.0.1:5000"
        },
        {
          "path_prefix": "/",
          "upstream": "https://example.com/"
        }
      ]
    },

    "proxy_protocol": {
      "enabled": true,
      "version": 1
    }
  },

  // メトリクス設定(オプション)
  "metrics": {
    "enabled": true,
    "listen": "127.0.0.1:9090"
  },

  // UI 設定(オプション)
  "ui": {
    "enabled": true,
    "listen": "127.0.0.1:9091",
    "remote_url": "https://tls-proxy-ui.example.com"
  }

雑な比較

https://canary.sora-devtools.shiguredo.app/ を nginx と TLS Proxy 経由でブラウザで表示した結果です。とりあえず十分な性能は出ていると判断しました。

nginx
TLS Proxy

ちなみにオリジナルは Cloudflare R2 経由です。

Cloudflare R2 (HTTP/2)

雑感

とにかく nginx はスゴイの一言です。本当によくできています。機能をとにかく絞って絞って自分たちが使う機能だけにしたとしても、山盛りでした。nginx 使えばいいなという気持ちに凄くなります。

ただ、自社製品専用の TLS Proxy を作って顧客に提供するというのが一つの目標ではあったので、それに向けて少しずつ改良していこうと思います。

今のところ OSS での公開予定はなく、Ubuntu 向けにのみ apt で提供する方針で考えています。まずは自社の検証サービスで運用していこうと思います。


systemd

systemd で動かす前提だったので、systemd を少し勉強したのですが、今は色々便利なんですね。Let's Encrypt を使ったり 443 で起動したりするのが簡単になってました。

とりあえずこんな感じで設定して安定して動いてます。(色々はしょってはいますが。

[Unit]
Description=tls_proxy - TLS Proxy for WebRTC SFU Sora
After=network-online.target
Wants=network-online.target

ExecStart=/usr/local/bin/tls_proxy /etc/tls_proxy/tlx_proxy.jsonc

LoadCredential=fullchain.pem:/etc/letsencrypt/live/example.com/fullchain.pem
LoadCredential=privkey.pem:/etc/letsencrypt/live/example.com/privkey.pem

# 再起動設定
Restart=on-failure
RestartSec=5

# セキュリティ設定
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
RestrictRealtime=true
RestrictNamespaces=true
LockPersonality=true
MemoryDenyWriteExecute=true

# 書き込み可能なディレクトリ(ACME ストレージ用)
ReadWritePaths=/var/lib/tls_proxy

# ケイパビリティ(ポート 443 でリッスンするため)
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE

# リソース制限
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target