TCP/QUIC相互変換のポートフォワードツールを書いた

TCP/QUICのポートフォワードツールを書いた。

概要

IETFで標準化が進められているトランスポートプロトコルQUIC。

UDPを利用しており、エンドポイントのIPアドレスが変わってもコネクションが切れなかったり、より良い再送制御が行えたりと長所は多くある。しかし、QUICをサポートしているアプリケーションプロトコル、実装が現状多くはない。

QUICの恩恵に預かるために、TCPとQUICを相互変換するポートフォワードツール 「t2q2t」 を書いた。(実態としてはただのProxy)
github.com


ただし、ハンドシェイク回数が増えるのでコネクション確立時のオーバーヘッドは高い

利用例

ユースケースとしては例えば:

f:id:ASnoKaze:20190818085413p:plain
クライアントとサーバそれぞれでt2q2tを実行する。

  • クライアント: TCPで0.0.0.0:2022でリッスンし、QUICで192.168.0.1:22に転送する
  • サーバ: QUICで0.0.0.0:2022でリッスンし、TCP127.0.0.1:22に転送する
# t2q2t <convert mode> <listen addr> <connect addr>

# クライアントサイド (TCP <-> QUIC)
$ ./t2q2t t2q 0.0.0.0:2022 192.168.0.1:2022

# サーバサイド (TCP <-> QUIC)
$ ./t2q2t q2t 0.0.0.0:2022 127.0.0.1:22

t2q2tとはローカルホスト通信を行い、ホスト間ではSSH over QUICを利用できるようになる。
(2重に暗号化処理を行うことになるが、QUICの特性であるロスリカバリの恩恵を受けるほか、IPアドレスが変わってもコネクションとかは切れないはず)

convert mode

サブコマンドは2種類。それぞれリッスン "アドレス:ポート"と、転送先 "アドレス:ポート" を引数にとる

  • t2q: TCPでリッスンして、QUICで転送する
  • q2t: QUICでリッスンして、TCPで転送する

その他

一応、SSHが問題なくフォワードされ。ログインできることは確認した。

注意事項として、t2q2tは、ALPN識別子として「t2q2t」を使用します。 他のQUIC実装と通信することは意図していない。

実装として、エラーハンドリングが雑なのでまだまだ怪しい。QUIC使う部分は毎回コネクション貼ってるので、ストリームの多重化を利用したいところ(8/22 対応済み)。簡単に挙動を確認したところ、単一コネクションになったので、複数TCPコネクションが帯域を食い合うこともなくなった。輻輳制御上も有利なはず。

性能評価や細かい改善とかはおいおい

t2q2tの読み方

決めてない...orz

curlのHTTP/3実験実装を触ってみる

First HTTP/3 with curl | daniel.haxx.se」で書かれている通り、curlがHTTP/3の実験実装を公開したので試す。

ライブラリとしてngtcp2を使う方法と、cloudflareのquicheを使う方法があるが今回はquicheを使う。

基本的には、Documentにかかれている通り。

なお、環境はUbuntu18.04

ビルド

boringSSLのビルド

$ git clone --recursive https://github.com/cloudflare/quiche
$ mkdir -p quiche/deps/boringssl/build
$ cd quiche/deps/boringssl/build
$ cmake -DCMAKE_POSITION_INDEPENDENT_CODE=on ..
$ make -j`nproc`
$ cd ..
$ mkdir .openssl/lib -p
$ cp build/crypto/libcrypto.a build/ssl/libssl.a .openssl/lib
$ ln -s $PWD/include .openssl

quicheのビルド

$ curl https://sh.rustup.rs -sSf | sh
$ source $HOME/.cargo/env

$ cd ../..
$ QUICHE_BSSL_PATH=$PWD/deps/boringssl cargo build --release

curlのビルド
サーバがレスポンスヘッダでHTTP/3対応を示すのに使われるalt-svcも有効にするために、"--enable-alt-svc"をつけてconfigureする

$ cd ..
$ git clone https://github.com/curl/curl
$ ./buildconf
$ ./configure --with-ssl=$PWD/../quiche/deps/boringssl/.openssl --with-quiche=$PWD/../quiche --enable-debug --enable-alt-svc
$ make -j`nproc`

うまく行っていれば、configure時に下記の通り表示される

  Alt-svc:          enabled
...
  HTTP3:            enabled (quiche)

試す

上記の通り、alt-svcを使わずに直にHTTP/3で接続しに行く場合は --http3-direct をつけてアクセスする

$ src/curl --http3-direct https://www.facebook.com/  -v -s -o /dev/null
* STATE: INIT => CONNECT handle 0x563e1077b528; line 1362 (connection #-5000)
* Added connection 0. The cache now contains 1 members
* STATE: CONNECT => WAITRESOLVE handle 0x563e1077b528; line 1403 (connection #0)
*   Trying 31.13.82.36:443...
* Connecting socket 4 over QUIC
* Sent QUIC client Initial, ALPN: h3-22
* STATE: WAITRESOLVE => WAITCONNECT handle 0x563e1077b528; line 1482 (connection #0)
* Connected to www.facebook.com () port 443 (#0)
* STATE: WAITCONNECT => SENDPROTOCONNECT handle 0x563e1077b528; line 1538 (connection #0)
* Marked for [keep alive]: HTTP default
* STATE: SENDPROTOCONNECT => PROTOCONNECT handle 0x563e1077b528; line 1553 (connection #0)
* quiche established connection!
* STATE: PROTOCONNECT => DO handle 0x563e1077b528; line 1572 (connection #0)
* Using HTTP/3 Stream ID: 0 (easy handle 0x563e1077b528)
(略)

パケットキャプチャをすると、ちゃんとIETF QUIC draft-22バージョンで接続していることが確認できた
f:id:ASnoKaze:20190807031738p:plain

動画上にコメントを表示する"弾幕"の仕様

W3Cの「Chinese Web Interest Group」から、Unofficial Draftとして「弹幕规范」(英語版: Bullet Chatting Proposal)というドキュメントが公開されています。

仕様上でも「use cases and requirements for Danmaku」と書かれている通り、動画上にコメントの弾幕を流すユースケースと新しいエレメントを定義するドキュメントのようです。China MobileやBilibili Inc.の方が共著として入っています。

国内ではニコニコ動画が有名ですが、中国ではBilibiliやAcFunといったサイトなどがこの弾幕機能を持つ動画共有サイトとして有名なようです。

このドキュメントでは、新しくbulletchatlistエレメントとbulletchat エレメントをRecommended APIとして定義しています。

  • bulletchatlist: コメントの表示領域
  • bulletchat: 各コメント
<bulletchatlist area="70" >
  <bulletchat mode="scroll" >This is Content</bulletchat>
  <bulletchat mode="bottom" >Fixed Content</bulletchat>
</bulletchatlist>

f:id:ASnoKaze:20190805001724p:plain
(引用: Bullet Chatting Proposal)

  • bulletchatlistは、表示領域を示すarea属性を持ちます。コメント同士を重ねて表示するallowOverlap属性を持ちます。
  • bulletchatは、そのコメントをどのように表示するかのmode (右から流れるscroll, 下に固定表示するbottomなど)。また、表示及び非表示時に発火するイベントなども定義されています。

Demo

公式のDemoとして動くものが作られています。
w3c.github.io

おわりに

すでにGithubW3C organization配下で文書が管理されています。Interest Groupということで勧告文書は出せないと思うのですが、この先どうなるのかは興味があります。

複数TLSコネクションの署名処理をまとめて行うBatch Signing

GoogleのDavid Benjaminさんにより「Batch Signing for TLS」という仕様が提出されています。

TLSではハンドシェイク中に証明書を持っていることを証明するために、対応する秘密鍵を用いて署名を行います。この署名処理(特にRSA署名)はCPUをたくさん消費するほか、秘密鍵をハードウェアモジュールに格納している場合はオーバーヘッドが高くなります。

この署名処理を、複数コネクション分をまとめて1回で行ってしまうのが「Batch Signing for TLS」です。

Acknowledgmentsに書かれているとおり、Roughtimeプロトコルに同じような機能があります

概要

複数のハンドシェイク中のコネクションから、署名を行うメッセージを集め、Merkleツリーを構成します。

葉を署名対象のメッセージのハッシュ値として、ルートにのみ署名を行います。そのため、複数のメッセージに対して1回の署名で済ませることができます。

メッセージ1, 2, 3をBatch Signingする場合
f:id:ASnoKaze:20190730180837p:plain

  • 葉の初期化: 各メッセージ1, 2, 3 のハッシュ値を偶数番号の葉とします。奇数番号の葉は乱数です。
  • ノードの計算: 各レベルごとに、左右の子を連結したハッシュをノードの値とします
  • ルートの計算: ルートまでノードの値を計算します。

ノードに署名をします。

署名を検証する場合、ルートを計算するのに必要なノードのみが与えられます。メッセージ2を検証する場合、必要なノードはt03, t10, t21です。このノードを順々に連結しハッシュを求めることでルートの値を計算でき、署名値を検証できます。

もちろん、検証する際に他のコネクション用のメッセージを取得できると問題です。そのため、署名するメッセージは偶数番号の葉で、奇数番号は乱数であり結合しハッシュを取ったもののみが他の人にも共有されるため、メッセージ自体がなんだったかそのハッシュ値さえわかりません。

SignatureScheme

Batch Signing用のSignatureSchemeも追加で定義します

       enum {
           ecdsa_secp256r1_sha256_batch(TBD1),
           ecdsa_secp384r1_sha384_batch(TBD2),
           ecdsa_secp521r1_sha512_batch(TBD3),
           ed25519_batch(TBD4),
           ed448_batch(TBD5),
           rsa_pss_pss_sha256_batch(TBD6),
           rsa_pss_rsae_sha256_batch(TBD7),
           rsa_pkcs1_sha256_legacy_batch(TBD8),
           (65536)
       } SignatureScheme

HTTPSで接続するための追加情報を格納するHTTPSSVCレコード

2020/07/19 追記
仕様に幾つかの変更があったため、新しく記事を書き直しました
asnokaze.hatenablog.com



HTTPSで接続する際に以下の情報を持っていると都合がよいです

  • SNIを暗号化するESNIの鍵情報など情報、(通常ESNI DNSレコードに記述される)
  • HTTP/2やHTTP/3で通信可能な事を示すAlt-Svc (通常、HTTPレスポンスヘッダやAlt-Svcフレーム、Alt-Svcレコードで提供される)

それらの情報を通知するのに、DNSに新しくHTTPSSVCレコードを追加する「HTTPSSVC service location and parameter specification via the DNS」という提案仕様がGoogleAkamaiの方の共著で提出されています。

また、このHTTPSSVCレコードではドメインのApexでも使用できるためApexでCNAME使いえない問題も解決できるほか、HTTP Strict Transport Security [HSTS] をクライアントに通知できるので初回接続のセキュリティーを改善します。

拡張可能でもあるため、今後もこのレコードに機能を追加することもできます。

HTTPSSVC レコードの利用例です。

 example.com.      2H  IN HTTPSSVC 0 0 svc.example.net.
 svc.example.net.  2H  IN HTTPSSVC 1 2 svc3.example.net. "hq=\":8003\" \
                                    esnikeys=\"...\""
 svc.example.net.  2H  IN HTTPSSVC 1 3 svc2.example.net. "h2=\":8002\" \
                                    esnikeys=\"...\""
  • example.comsvc.example.netからも提供できることを示します。
  • svc3.example.netからHTTP/3を8003ポートで提供できることを示します(また必要なesnikeysを示します)
  • svc2.example.netからHTTP/2を8002ポートで提供できることを示します(また必要なesnikeysを示します)

HTTPSSVCレコードは存在自体がHSTSを示すので、クライアントはHTTPのリンクもHTTPSで接続しに行きます。

フォーマット

HTTPSSVCは、2つの数字がまずならびます。

仕様では、拡張としてesnikeysでESNI用の鍵も付与できるようになっています。

TCP Slow Startを改善する HyStart++について

20190725追記
draft-01 で計算式に変更が入っています


TCP Slow Startを改善する 「HyStart++: Modified Slow Start for TCP」というdraftがMicrosoftの方より提出されている。

HyStart++はすでにWindowsで実装されている他、HyStartもLinuxのCubicに実装されている。

Slow StartとHyStart++

TCPはコネクション開始直後は送信するデータ量は少なく、徐々に2倍にしていく。パケットロスが発生したタイミングでこのスロースタートは終わることになる。

2倍, 2倍で送信するデータ量を増やしていくと適切な送信量を大幅に超えてしまいうる。大きく超えると多くのパケットロスが発生してしまい、デメリットが大きいため適切なタイミングで線型的にデータ量を増やしていきたい。特定の閾値ではなく、RTTの悪化をトリガーとして線型増加するLimited Slow-Start(RFC3742)に移行するのがHyStart++です。

f:id:ASnoKaze:20190712222721p:plain

各ラウンド毎に、前回のRTTよりもEta(今回のRTT/8 を 4~16msec 以内に収めた値) msec RTTが悪化していた場合はLimited Slow-Startに移行する。

      MIN_SSTHRESH = 16
      MIN_ETA = 4 msec
      MAX_ETA = 16 msec
         if (cwnd is >= MIN_SSTHRESH)
            Eta = clamp(MIN_ETA, currentRoundMinRTT / 8, MAX_ETA)
            if (currentRoundMinRTT >= (lastRoundMinRTT + Eta))
               exit slow start and enter LSS

HyStart++?

参照されているHyStartのドキュメントが有料で読めず、Kernelに貼られているURLもリンク切れになっていた

HyStartで定義されているアルゴリズムが2つあるが、有効だったそのうち一つがHyStart++として改良されている?わからなかった。

提案仕様「HTTP Transport Authentication」について

HTTPレイヤにおいて、使用しているトランスポートレイヤの認証を行う「HTTP Transport Authentication」という仕様がGoogleのDavid Schinazi氏から提案されています。軽く読んだのでメモがてら



この提案では新しいリクエストヘッダ、Transport-Authenticationヘッダを定義します。サーバはこのヘッダをもとに接続相手が正しいクライアントかトランスポートレイヤ的に確認することができます。後述の通りTLSの利用を前提としています

もちろん、この機能は既存のHTTP認証を置き換えるものではなく補完する機能となります。

既存のHTTP認証だけでは、トランスポートのコネクション相手が本当に正しいかはわからず、途中でTLSがほどかれている可能性もあります(サーバ認証するのでほぼないでしょうが)。逆に、TLSのクライアント認証だけではどのAuthorityに対するHTTPリクエストかはわからない状態で認証することになります(HTTP/2ではSNIとリクエスト先のhostが一緒とは限らないため)。

下記の記事でも紹介したとおり、HTTP/3上で別の通信を行うに際して利用することを想定しています。

David Schinazi氏は、MASQUEプロトコルの提案者でもあり、まさにMASQUEプロトコルで利用していた認証方式を別出しした形に近いです。

認証とTransport-Authenticationヘッダ

このHTTP Transport Authenticationでは認証用に、事前に共有鍵をサーバとクライアントを共有している、もしくはクライアントの公開鍵をサーバが知っている前提になります。

TLS keying material exporter (rfc5705)の仕組みを利用して、該当のTLSコネクション由来の乱数を取り出しNonceとして利用します。この取り出したNonceを共有鍵、もしくは秘密鍵で暗号化することで認証用データを作ります。作った認証用データをTransport-Authenticationヘッダに入れて送信します。

それでは、Transport-Authenticationヘッダの中身を見ていきます。

   Transport-Authentication: Signature u="am9obi5kb2U=";a=1.3.101.112;
   p="SW5zZXJ0IHNpZ25hdHVyZSBvZiBub25jZSBoZXJlIHdo
   aWNoIHRha2VzIDUxMiBiaXRzIGZvciBFZDI1NTE5IQ=="

(表示上改行を入れています)

ヘッダにはいくつかのディレクティブがつきます

  • u: 認証するユーザidをBase64で符号化したもの
  • a: しようする暗号アルゴリズムのOID
  • p: 認証用データ。Nonceを暗号化したもの

HTTPレイヤからトランスポートを認証するメリット

MASQUEプロトコルの仕様でも書かれている通り、TLSのクライアント認証とは異なり通信経路上の第三者に認証を行っていることがわからないというメリットがあります。