Alcogy Tech Blog
SvelteKit・Cloudflare・AI・アーキテクチャ設計など、 現場で得た技術的な知見を発信しています。
バックエンドへの接続確立に失敗したら、別のバックエンドへ即座にフェイルオーバーするリトライを実装します。サーキットブレーカーの役割はすでにパッシブ/アクティブヘルスチェックが担っているため、今回はリトライに専念します。あわせて、リトライにバックオフを入れなかった設計判断と、その判断が小規模と大規模(Cloudflare級)のフリートで正反対の意味を持つ理由を扱います。
新規コネクションの受け付けだけを止め、転送中のコネクションは自然に終わるまで待つグレースフルシャットダウンを実装します。JoinSetによるコネクション追跡とtokio::select!の組み合わせに加えて、シグナルハンドラを一度登録すると2回目以降のシグナルが誰にも届かず消えてしまうという、実機検証で見つけた落とし穴を扱います。
HTTPのkeep-aliveと違い、生のバイト列をそのまま転送するL4プロキシでは「使い終わった接続の再利用」は成立しません。代わりに、バックエンドへの接続をあらかじめ張っておき、新規クライアント接続がTCPハンドシェイクを待たずに済むようにする、事前ウォームアップ型のコネクションプールを実装します。
定期的なTCP接続確認とは別に、実際に転送したコネクションの成否からバックエンドの健全性を判定するパッシブヘルスチェックを実装します。「バックエンドへの接続失敗」と「転送中のI/Oエラー」を区別して後者をバックエンドの責任にしない設計判断と、Cloudflare pingoraの実装との比較を扱います。
バックエンドの生死を定期的なTCP接続で確認し、死んでいるバックエンドをロードバランサーの選択候補から動的に除外する仕組みをRust+tokioで実装します。生死フラグの共有方法、並行したタイムアウト付き接続確認、「選べないかもしれない」という変化をtraitシグネチャとlet-else構文でどう表現するかを扱います。
TCPコネクションを複数のバックエンドへ振り分けるロードバランサーをRustで実装します。アルゴリズムを実行時に差し替えられるようトレイトオブジェクトで抽象化したうえで、Round Robin・Random・Least Connections・Weightedの4種類を実装し、それぞれのアルゴリズム特有の設計判断(同着時の挙動やMutexが必要になる条件など)を扱います。あわせてモジュール分割とインラインユニットテストの設計にも触れます。
復号したEncryptedExtensions以降のメッセージを読み進め、サーバのFinishedを検証してからクライアント自身のFinishedを送り返すところまでを実装します。動的に積み上がるトランスクリプトハッシュの扱いと、Finishedメッセージ特有の鍵導出を扱います。
EncryptedExtensions以降のTLS 1.3ハンドシェイクメッセージはAES-128-GCMで保護されています。Handshake Traffic SecretからAEAD鍵/IVを導出し、レコードごとのnonce構成、TLSInnerPlaintextの復元、そして実サーバとの通信で必須になるChangeCipherSpecレコードの扱いまでを実装します。
TLS 1.3のEncryptedExtensions以降を暗号化するために必要なHandshake Traffic Secretを導出します。X25519のECDH共有鍵計算、RFC 8446 §7.1のHKDF-Extract/Expand-Label/Derive-Secretによる鍵スケジュールの実装、そしてRFC 8448の実例トレースを使って実サーバなしに導出結果を検証する方法までを扱います。
TLS 1.3ハンドシェイクの最初の往復、ClientHelloの送信とServerHelloの受信・検証を実装します。鍵交換グループはX25519のみ、暗号スイートはTLS_AES_128_GCM_SHA256のみに絞り、鍵ペア生成だけOpenSSLの力を借ります。TLSレコードとは別の層であるハンドシェイクメッセージのフレーミングと、ServerHelloがHelloRetryRequestを兼ねる仕組みについても扱います。
TLS 1.3のレコード層のうち、暗号化を伴わないTLSPlaintextのフレーミング(RFC 8446 §5.1)を実装します。TCPはメッセージ境界を保持しないため、最大16384バイトのレコードを512バイト単位のTCPセグメントの連なりから正しく復元する`RecordLayer`の設計と、複数セグメントに渡る転送で表面化したTCP側の弱点についても扱います。
TCPヘッダのparse/buildの上に、stop-and-wait方式によるシーケンス番号管理・タイムアウト再送・簡易フロー制御を実装します。合わせて、OSの標準ソケットAPIを経由せず生ソケットで自前のTCPコネクションを実装したときに必ず踏む、「送信元マシン自身のカーネルが身に覚えのないセグメントへRSTを送り返してしまう」という設計上の制約と、その回避方法についても扱います。
TCPヘッダはICMP/UDPと違い、フラグ・シーケンス番号・可変長のオプションを持ちます。3-wayハンドシェイクに必要な範囲でヘッダの型を設計し、UDPと同じ疑似ヘッダ方式のチェックサムを再利用しつつ、UDPには存在する「0はチェックサム未使用」という読み替えがTCPには不要である理由にも触れます。最後に、実際にLAN上のホストとSYN/SYN-ACK/ACKを交わすところまで確認します。
Rustの「値の持ち主は1人まで」という所有権ルールは、複数の場所から同じ値を参照・共有したい場面ではそのままでは足りません。この制約を緩めるRc/Arc(複数の持ち主)とRefCell/Mutex(内部可変性)、その安全性を支えるアトミック命令とSend/Sync、実行時に型を確定させるdynについて、それぞれが解決する問題を切り分けて整理します。
TCPコネクションを別のTCPコネクションへ中継する処理をRust+tokioで実装します。tokioのfeatureを用途別に絞って導入する方針や、`tokio::io::copy_bidirectional`が単純な双方向コピーではなく、TCPの片方向クローズ(half-close)を個別に伝播する設計になっている点を扱います。