Wednesday, December 2, 2020

komake: Make の -j オプションに潜む罠とその解決策

ビルドツールのダジャレの大家と言えば @shinh さんですが、それはさておき、皆さんは今でも Make を使ってビルドすることが多いと思います。かく言う私も、その一人。

最近は CPU のコア数も多いですから、当然 -j 16 とか、やりたいわけです。大きいプロジェクトになればなるほど、威力絶大ですね。

ですが、ここで問題がひとつ。大規模プロジェクトでは Makefile が別の Makefile を呼び出すような依存関係が良く見受けられます。この際、ターゲット間の依存関係で菱形が存在すると(例: ターゲット sub1 と sub2 が shared に依存)、make shared が make sub1 と make sub2 から同時に起動されることが起こりえます。CMake で生成した Makefile の場合も、ターゲット毎に make を起動しますね。

二重起動が発生すると、ほとんどの Makefile は、同時起動されることを想定していませんから、ビルドは失敗したり、成果物が壊れたりしてしまいます。

実際に、以下のような Makefile を書いてみると、make shared が同時に動いてしまうことを確認できます(sleep 1が2回表示されていることに注目)。

$ cat Makefile
all: proj
proj: sub1 sub2
	touch $@
sub1:
	$(MAKE) shared
	touch $@
sub2:
	$(MAKE) shared
	touch $@
shared:
	sleep 1
	touch $@
clean:
	rm -f proj sub1 sub2 shared

$ make -j2
/Applications/Xcode.app/Contents/Developer/usr/bin/make shared
/Applications/Xcode.app/Contents/Developer/usr/bin/make shared
sleep 1
sleep 1
touch shared
touch shared
touch sub1
touch sub2
touch proj

どうしたらいいでしょう。

先に書いたように、問題は同一ターゲットを引数にとる make が重複起動されてしまうところです。ならば、make の並走度を別途制限すればいいのではないでしょうか。ある make が終了するためには、その make が呼び出した make が全て終了している必要があることを加味すると、単純に、呼び出し階層毎の make の同時実行数を1に制限してしまえば、この問題が解決するはずです。

ジャジャーン!!! と言うわけで書いたのが make のラッパースクリプト komake です!

わずか41行のスクリプトにして、求められる機能を実現しています。komake を使って先の Makefile を実行すると、ほら、このとおり!!!

$ komake -j2
komake shared
komake shared
sleep 1
touch shared
touch sub1
make[1]: `shared' is up to date.
touch sub2
touch proj

komake shared は2回呼び出されていますが、実際のビルド作業である sleep 1 と touch shared の呼び出しが1回に減少したことがわかります。これは、1回目の komake shared が終了した後に2回目の komake shared が呼び出され、既に「shared」ファイルが存在したから、ビルド作業は何も走らなかったからです。

完璧ですね!! すばらしい!!!!

なお、注意点として、多重起動されロックを取ろうとしている komake も make のジョブスロットを消費します。ですので、komake を使う場合には、-j オプションに渡す引数を CPU コア数 + ロックにひっかかりそうな make の数(たとえば4)等にセットするとよいでしょう。

ちなみに、名前の komake は、「こまけー問題なおしてるな」というセルフツッコミに由来します。細かな問題なのですが、大規模プロジェクトにおいて切実な make の待ち時間問題を解決する、かゆいところに手の届くツールになっていると思います。

。。。。。とか書いてるうちに、同僚の @gfx が、問題となっていたプロジェクトのビルドツールを Ninja に移行する作業を終わらせてしまいました。完敗だ。これはKO負けですね。

Thursday, June 18, 2020

QUICむけにAES-GCM実装を最適化した話 (2/2)

前半で述べたように、OpenSSLのAEAD暗号器は、長いAEADブロックの処理を前提に作られています。平文の暗号化処理においては理論上の上限にあたる速度を叩き出す一方、事前処理と事後処理、および呼出オーバーヘッドについては、あまり最適化が図られているとは言えません。これは、AEAD暗号の主な使用用途が、これまでTLSという長いAEADブロックを使う(ことが一般的な)プロトコルであったことを反映していると言えるでしょう。

一方、QUICにおいては、UDPパケット毎に独立した、短いAEADブロックを暗号化する必要があり、したがって、次のような速度向上の機会があることが分かります。

AEAD処理をひとつの関数にまとめ、事前処理と事後処理を、パイプライン化されスティッチングされた暗号処理と並行に走らせることができれば、AEADブロックが短くても、理論値に近いスループットを発揮するような、AES-GCM実装を作ることができる(前半より引用)

この条件を満たすような関数を実装し、ボトルネックをつぶしていって速度向上を図るというのは一案です。しかし、往々にして、そのような対症療法的なプログラミングスタイルでは、何回もの変更に伴う手戻りが発生したり、必ずしも最適でないコードが成果物の一部に残ったりしがちです。

より効率的な設計手法はないものでしょうか。

■QUIC向けAES-GCM実装「fusion」の設計方針

幸いなことに、AES-GCMについては、第9世代Core CPUにおけるボトルネックがAES-NIであり、そのスループットの理論上の上限が64バイト/40クロックであることが分かっています。スティッチングを用いたAES-GCM実装が、暗号化処理中、AES-NIを最高速度で回しつつ、他の演算ユニットを用いてGCMのハッシュ計算を行うという手法であることも、先に述べたとおりです。

ならば、AES-NIを常時実行しつつ、その合間をぬって、AEADの事前処理、事後処理を含む他のあらゆる処理を行うようにすれば、理論上の上限値に迫るようなAES-GCM実装が作れるのではないでしょうか。

このような考えに基づき、以下のような特徴をもつAES-GCM暗号ライブラリ「fusion」を作成することにしました:
  • できるだけ長い間、6*16バイト単位でAES-NIを実行する
  • その間に、AAD(=事前処理)を含む、任意の長さのGCMハッシュ計算を行う
  • 複雑な設計をメンテ可能とするために、アセンブリではなくCで記述する
  • AEADブロック全体にわたって、GCMハッシュの事前計算を行う。それにより、reductionの負荷を下げる
  • パケットヘッダ暗号化(パケット番号暗号化)に必要なAES演算を重畳する

AES-GCM暗号化の典型的なデータフローを可視化してみましょう。第一の図が、古典的な(OpenSSLのような)暗号化部分に注力したアプローチです。第二の図が、fusionのアプローチです。横軸が時間軸で、縦に並んでいる処理は同時実行(スティッチング)されています。fusionでは、より多くの処理がスティッチングされることがわかります。



以下が、fusion.cの暗号化のホットループです。gfmul_onestepは、1ブロック分のGCMハッシュの乗算演算を行うインライン関数です。6ブロック分(bits0〜bits5)のAES計算をする間に、gdata_cntで指定された回数だけgfmul_onestepを呼び出していることがわかります。
#define AESECB6_UPDATE(i) \
    do { \
        __m128i k = ctx->ecb.keys[i]; \
        bits0 = _mm_aesenc_si128(bits0, k); \
        bits1 = _mm_aesenc_si128(bits1, k); \
        bits2 = _mm_aesenc_si128(bits2, k); \
        bits3 = _mm_aesenc_si128(bits3, k); \
        bits4 = _mm_aesenc_si128(bits4, bits4keys[i]); \
        bits5 = _mm_aesenc_si128(bits5, k); \
    } while (0)
#define AESECB6_FINAL(i) \
    do { \
        __m128i k = ctx->ecb.keys[i]; \
        bits0 = _mm_aesenclast_si128(bits0, k); \
        bits1 = _mm_aesenclast_si128(bits1, k); \
        bits2 = _mm_aesenclast_si128(bits2, k); \
        bits3 = _mm_aesenclast_si128(bits3, k); \
        bits4 = _mm_aesenclast_si128(bits4, bits4keys[i]); \
        bits5 = _mm_aesenclast_si128(bits5, k); \
    } while (0)

    /* run AES and multiplication in parallel */
    size_t i;
    for (i = 2; i < gdata_cnt + 2; ++i) {
        AESECB6_UPDATE(i);
        gfmul_onestep(&gstate, _mm_loadu_si128(gdata++),
                      --ghash_precompute);
    }
    for (; i < ctx->ecb.rounds; ++i)
        AESECB6_UPDATE(i);
    AESECB6_FINAL(i);

コードを注意深く読んだ方は、bits4の計算だけ、異なる鍵を使うようになっていることに気づいたかもしれません。これが、パケットヘッダ暗号化のためのAES計算を重畳するための工夫です。

■パケットヘッダ暗号化

パケットヘッダ(パケット番号)の暗号化は、QUICやDTLS 1.3といった新世代のトランスポートプロトコルに見られる機能です。パケットヘッダを暗号化することで、傍受者による通信内容の推測をより難しくしたり、中継装置(ルータ)が特定の通信パターンを前提にしてしまうことによりトランスポートプロトコルの改良が困難になること(ossification)を防ぐ効果が期待されています。

なぜ、パケットヘッダ暗号化のAES計算を重畳するのか。それは、6ブロック分のAES計算を一度に行う以上、パケット長を96で割った余りが65から80の間にならない限り、使われないスロットが発生するためです。その余ったスロットをパケットヘッダ暗号化のAES演算に使うことで、パケットヘッダ暗号化のコストを隠蔽するのが目的です。

パケットヘッダ暗号化を重畳した場合のデータフローを、以下に示します。

■ベンチマーク

では、ベンチマーク結果を見てみましょう。

青の棒は、OpenSSLのAES-GCM処理のうち、事前処理と事後処理を含まないスループットを、赤の棒は、両者を含んだトータルでのスループットを表しています。黄色はfusionのトータルスループット、緑は、パケットヘッダ暗号化に必要な演算を重畳した場合の値です。


まずは、最近のIntel製CPUである、Core i5 9400の値を見てみましょう。

AEADブロックサイズが16KBの場合、OpenSSLの事前事後処理を含まないスループットとfusionのスループットが、いずれも6.4GB/sという理論上の上限に達していることが分かります(微妙なズレは、CPUクロック制御の精度に起因するものです)。OpenSSLの事前事後処理を含むスループットは若干遅い6.2GB/sですが、TLSにおいて、事前事後処理を最適化しないオーバーヘッドは3%以下である、という風に読むこともできます。

一方で、AEADブロックサイズが1440バイトの場合、差は顕著です。OpenSSLのトータルスループットが4.4GB/sと、理論値の約70%にまで落ち込むのに対し、fusionは理論値の90%を超えるスループットを発揮します。また、パケットヘッダ暗号化によるオーバーヘッドが1%以下なのも見てとることができます。

AMD Ryzenに目を向けると、AEADブロックサイズ1440バイトの場合のみならず16KBの場合でも、fusionが勝っていることが読み取れます。これは、RyzenのAES-NIのスループットがPCLMULと比較して高いため、ボトルネックがPCLMULに代表されるGCMハッシュ計算の側に移動したものと考えられます。fusionは、想定されるAEADブロック全体にわたって事前計算を行うことで、GCMハッシュ演算のうちreductionの回数を削減しているので、ブロックサイズ16KBの場合にも差がついたと考えることができます。

■考察

カーネル・ネットワークカードのUDP処理が最適化された場合、暗号処理のコスト差が問題となって、TLSよりもQUICのほうがCPU負荷が高くなる、という問題がありました。この問題について、QUICを始めとする暗号化トランスポート向けに最適化したAES-GCM実装を準備することで、大幅な改善が可能であることを示しました。fusionをQUICの暗号ライブラリとして使った場合の詳細は本稿では紹介しませんが、TCPとUDPでGSOハードウェアオフロードがある環境において、パケットサイズ9KBならQUICが優位、パケットサイズ1.5KBでもQUICのオーバーヘッドはTLS+5%程度だという測定結果を得ています(参照: h2o/quicly PR #359)。

あわせて、
  • パケットヘッダ暗号化のコストは(少なくとも送信側においては)特に問題視するレベルではないこと
  • アセンブリを用いる場合と比較して、C言語を用いることで、最善ケースのスループットを保ったまま、より高度な設計による暗号ライブラリが開発可能であること
を示しました。

今回開発したAES-GCM実装「fusion」は、昨日、我々が管理するTLSスタックであるpicotlsにマージされ、使用可能になっています。fusion、あるいはそれに類する実装手法を用いることで、インターネット上の通信が、より低コストに、より安全になっていくことを期待します。

末筆ですが、fusionを開発するにあたり、光成(@herumi)さんにアドバイスを、吉田(@syohex)さんにベンチマークでご協力をいただきました。この場を借りて御礼申し上げます。

Monday, June 15, 2020

QUICむけにAES-GCM実装を最適化した話 (1/2)

4月末に、会社のほうで「Can QUIC match TCP’s computational efficiency?」というブログエントリを書きました。我々が開発中のQUIC実装であるquiclyのチューニングを通して、QUICのCPU負荷はTLS over TCP並に低減可能であろうと推論した記事です。この記事を書く際には、Stay Homeという状況の中で、手元にあった安いハードウェアを使ったのですが、その後、10gbe NICを入手し、ハードウェアによるUDP GSOオフロード環境でのパフォーマンスを確認していくと、OpenSSLのAES-GCM実装がボトルネックになることがわかってきました。

TCP上で通信するTLSでは、一般に、データを16KB単位でAEADブロックに分割して、AES-GCMを用いてAEAD暗号化します。一方、UDPを用いるQUICでは、パケット毎にAES-GCMを適用することになります。インターネットを通過することができるパケットサイズは高々1.5KBなので、QUICのAEADブロックサイズはTLSと比較して1/10以下となります。

両条件について、OpenSSLのAES-GCM実装のスループットを比較したところ、4GHzの第9世代Intel Core CPUを使った場合、AEADブロックサイズ16KBにおいては約6.4GB/sなのに対し、AEADブロックサイズ1440バイトにおいては、4.4GB/s程度しか出ないことが分かりました。ハードウェアGSOオフロードが可能な環境ではCPU負荷の半分弱が暗号処理コストになるので、暗号処理で7割のスループットしか出ないのは、QUICの足かせになります。

それにしても、なぜ、これほど大きな速度差が発生するのでしょう。

その答えを理解するには、最適化されたAES-GCM実装が、一般に、どのようなものかを知っておく必要があります。

■AES-NIとパイプライン処理

まず、AES-GCMのうち、暗号処理であるAES実装の最適化手法を見てみましょう。

最近のx86 CPUは、たいてい、AES-NIというAES処理専用の命令に対応しています。128bitのAES暗号化においては、AES-NI命令を10回発行することで、16バイトの暗号化を行うことが可能です。第9世代のIntel Core CPUにおいては、AES-NI命令のレイテンシは4クロックなので、10*4=40クロックで16バイトの暗号化が可能になります。

4GHzでのスループットを計算してみると、4GHz / 40clock * 16byte = 1.6GB/sになります。

あれ、先ほどの6.4GB/sと比べると1/4の値です。なぜでしょう?

実は、x86 CPUはAES-NIをパイプライン処理します。そのため、依存関係のない(=別のAESブロックを処理する)AES-NI命令を1クロックごとに1個発行することが可能なのです。

つまり、16バイトの基本ブロック単位で処理するのではなく、AES-NI(ブロック1用)、AES-NI(ブロック2用)、AES-NI(ブロック3用)、AES-NI(ブロック4用)、AES-NI(ブロック1用)、...のように、16バイトのブロック4つ分のAES-NI命令を並行に発行し続けることで、64バイト分の暗号化を4*10=40クロックで終えていくことができるのです。

4GHzでのスループットを再び計算すると、$GHz / 40clock * 16byte * 4 = 6.4GB/s。

毎クロック1命令発行、16バイトの暗号化に10命令必要ですから、これが理論上の上限値になります。

でも、ちょっと待ってください。6.4GB/sは、暗号処理であるAESのスループットであって、認証符号であるGCMの負荷を含んでいません。GCMの負荷は一体どこに行ってしまったのでしょう

■AES-GCMのスティッチング

GCMはガロア体における乗算を用いる認証符号で、x86 CPUでは、PCLMULQDQというキャリーレス乗算命令を利用する最適化が知られています。さらに、式を変形することで、16バイトあたりのPCLMULQDQ命令発行回数を5回に減らせることが、また、事前計算を行えば、16*nバイトあたりのPCLMULQDQ命令発行回数を3*n+2回まで減らせることが知られています(参照: https://crypto.stanford.edu/RealWorldCrypto/slides/gueron.pdf)。

PCLMULQDQ命令は、7クロックのレイテンシがありますが、AES-NIとは同時に発行可能なので、AES命令とPCLMULQDQをほどよく織り交ぜるようなプログラムを書くことで、AESとGCMを並列に計算することが可能です。

IntelのCPUにおいては、若干GCMの方がAESよりも軽いので、ボトルネックはAESになり、AES-GCMでもAES同様のスループット6.4GB/sが期待できます。

■OpenSSLのaesni_ctr32_ghash_6x関数

以上を踏まえ、OpenSSLのAES-GCM実装であるaesni_ctr32_ghash_6x関数を見てみましょう。

この関数は、perlスクリプトを用いて生成されるアセンブリコードですが、以下のような、AES-NI命令とPCLMULQDQ命令が織り混ざる構成になっています。また、AES-NI命令について、同じラウンドキーを違う引数($inout)に適用している、つまり、複数ブロックの暗号化を同時に行なっていることが分かります。命令の目的によってインデントを変えるなどの工夫も興味深いところです。

vpclmulqdq \$0x01,$Hkey,$Ii,$T2
    lea  ($in0,%r12),$in0
      vaesenc $rndkey,$inout0,$inout0
     vpxor 16+8(%rsp),$Xi,$Xi # modulo-scheduled [vpxor $Z3,$Xi,$Xi]
    vpclmulqdq \$0x11,$Hkey,$Ii,$Hkey
     vmovdqu 0x40+8(%rsp),$Ii # I[3]
      vaesenc $rndkey,$inout1,$inout1
    movbe 0x58($in0),%r13
      vaesenc $rndkey,$inout2,$inout2
    movbe 0x50($in0),%r12
      vaesenc $rndkey,$inout3,$inout3
    mov  %r13,0x20+8(%rsp)
      vaesenc $rndkey,$inout4,$inout4
    mov  %r12,0x28+8(%rsp)
    vmovdqu 0x30-0x20($Xip),$Z1 # borrow $Z1 for $Hkey^3
      vaesenc $rndkey,$inout5,$inout5
注意深い方は既にお気づきかもしれませんが、関数名の6xは、128bitのブロックを6ブロック単位で(つまり、96バイト単位で)AES-GCM符号化を行なっていることに由来します。

■なぜOpenSSLは短いAEADブロックの処理が苦手なのか

このように、丁寧に最適化されたコードであるにもかかわらず、なぜ、OpenSSLは短いAEADブロックの処理が苦手なのでしょうか。2つの要因が考えられます。

第一の要因は、関数呼び出しのオーバーヘッドです。OpenSSLのAEAD処理は、EVPと呼ばれるレイヤで抽象化されています。ひとつのAEADブロックを暗号化するには、EVP_EncryptInit_ex、EVP_EncryptUpdate、ENP_EncryptFinal、という3つの関数を介して、AES-GCM固有の処理を呼び出す必要があります。

第二の要因は、AES-GCMの事前処理と事後処理がパイプライン化されていない点です。先に紹介したaesni_ctr32_ghash_6x関数は、6.4GB/sという理論値を叩き出す、文句のつけどころのない関数です。しかし、AES-GCMにおいては、暗号化以外にも、AEADブロック毎に、AAD(認証つき平文)をGCMのコンテクストに入力したり、最終的なタグを計算するなどの処理が必要です。これらの付随する処理の負荷は、AEADブロックサイズが小さくのればなるほど、相対的に大きくなります。

これらの問題を指摘することは簡単です。

なるほど、AEAD処理をひとつの関数にまとめ、事前処理と事後処理を、パイプライン化されスティッチングされた暗号処理と並行に走らせることができれば、AEADブロックが短くても、理論値に近いスループットを発揮するような、AES-GCM実装を作ることができるでしょう。

しかし、上述したように、パイプライン化・スティッチングされたコードは、既に相当複雑です。ここにさらに、事前処理や事後処理を重畳することなどできるでしょうか。できたとして、保守可能なプログラムになるでしょうか


次回に続きます。



注: スロースタート時には、より小さなブロックサイズを使って実効レイテンシを改善する場合もあります(参照: https://www.slideshare.net/kazuho/programming-tcp-for-responsiveness

Thursday, April 16, 2020

C言語で配列の要素数を安全に数える話

C言語で配列の要素数を数えるイディオムってのがあって、
sizeof(array) / sizeof(array)
なんだけど、配列名が長くなって、たとえば
sizeof(var.that_has_an_array.as_a.member) /
    sizeof(var.that_has_an_array.as_a.member[0])
とかになるとカオス。

なので、ベンダーによっては、
#define _countof(array) (sizeof(array) / sizeof(array[0]))
みたいなマクロを提供していたりするんだけど、こうやって、何も考えずに使えるようにしていくと、配列ではなくポインタを引数に渡しちゃって、サイズ計算ミスって変な動作する懸念が増してくる。

なので、Twitterで

と聞いたところ、mattnさんから

と教えてもらったので、
#define PTLS_BUILD_ASSERT(cond) \
    ((void)sizeof(char[2 * !!(!__builtin_constant_p(cond) || (cond)) - 1]))

#define PTLS_ASSERT_IS_ARRAY(a) \
    PTLS_BUILD_ASSERT(
        __builtin_types_compatible_p(__typeof__(a[0])[], __typeof__(a)))

 #define PTLS_ELEMENTSOF(x) \
    (PTLS_ASSERT_IS_ARRAY(x), sizeof(x) / sizeof((x)[0]))
こんな感じにして取り込みました。これで、先の冗長な例も
PTLS_ELEMENTSOF(var.that_has_an_array.as_a.member)
と書くことができる。便利。

完全な変更は add PTLS_ELEMENTSOF for counting the number of elements in an array by kazuho · Pull Request #301 · h2o/picotls をご覧ください。


PS. 配列かポインタかの確認方法については、yuguiさんから下のようなコンパイラ非依存の解も教えてもらったのですが、残念ながらコンパイル時に動かすことが難しいようでした。実行時判定が必要なケースなら、この方法のほうがいいかも。

Wednesday, March 11, 2020

2020年にDSRロードバランス環境を作る方法

TCPの時代もそうだったんですが、QUICにおいても、Linux等でソフトウェアロードバランサをipvsadmとDSR (direct server return, a.k.a. direct routing) を実現することは有効な手段です。

なんだけど、ipvsadm界隈はHA前提の込み入った設定か古いドキュメントしか見つからなかったので、最低限の検証環境のセットアップ方法を、ここにまとめる次第です。

ロードバランサ:
echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward
sudo ifconfig eth0:0 $VIP netmask 255.255.255.255 broadcast $VIP up
sudo route add -host $VIP dev eth0:0
sudo ipvsadm -A -t $VIP:443 -s rr
sudo ipvsadm -a -t $VIP:443 -r $SERVER1 -g
sudo ipvsadm -a -t $VIP:443 -r $SERVER2 -g
sudo ipvsadm -a -t $VIP:443 -r $SERVER3 -g
...

要は、フォワーディングを有効にして、自分自身へのパケットを他のサーバにフォワードするような変態行為をするからspoofing対策フィルタをオフにして、次にipvsadmでテーブル作ってサーバ追加する。

サーバ:
echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward
echo 0 | sudo tee /proc/sys/net/ipv4/conf/eth0/rp_filter 
echo 2 | sudo tee /proc/sys/net/ipv4/conf/eth0/arp_announce
echo 1 | sudo tee /proc/sys/net/ipv4/conf/eth0/arp_ignore
sudo ifconfig lo:0 $VIP netmask 255.255.255.255 broadcast $VIP up
sudo route add -host $VIP dev lo:0

要は、フォワーディング有効にして、VIPがARPでアナウンスされたり、応答したりしないように設定して、loにVIPをつけて、eth0で受信したパケットがlo:0に転送されるように設定する。

これで動く。

Wednesday, August 14, 2019

H2O version 2.2.6, 2.3.0-beta2 released, includes security fixes

H2O version 2.2.6 and 2.3.0-beta2 have been released.

This release addresses a series of DoS attack vectors that have been recently found on a broad range of HTTP/2 stacks.

Specifically, H2O had been deemed vulnerable to the following, and fixed:

* CVE-2019-9512 (Ping Flood)
* CVE-2019-9514 (Reset Flood)
* CVE-2019-9515 (Settings Flood)

Users of previous versions of H2O are advised to update to the recent versions.

For more information, please refer to issue 2090: HTTP/2 DoS attack vulnerabilities CVE-2019-9512 CVE-2019-9514 CVE-2019-9515.

Tuesday, July 30, 2019

HTTP のプライオリティが大きく変わろうとしている話(その他 IETF 105 雑感)

先週、モントリオールで開催された IETF 105 に参加してきました。

いろんなことがあったのですが、個人的に一番大きかったのは、HTTP/3 からプライオリティ(優先度制御)まわりの仕様を落とすことが決定したこと。

HTTP/3 は、トランスポートプロトコルである QUIC の上で動作する、次世代の HTTP プロトコルです。その設計は、QUIC ワーキングググループが、HTTP ワーキンググループから委託され、HTTP/2 の機能を移植する、という形式を取っています。

ところが、5月にロンドンで開催された QUIC ワーキンググループの中間会議で、一部参加者から HTTP/3 の優先度制御に対する不満が表明されたのです注1。それを受けて、QUIC ワーキンググループでは、HTTP/3 の優先度制御にあった HTTP/2 のそれとの差異を少なくする作業を進める一方、HTTP ワーキンググループでは、IETF 105 において、プライオリティをどうするか、議論することになっていました。

HTTP/2 の優先度制御を十分にサポートしていないサーバが多いことは事実です。まともに実装したサーバを動かしている大規模事業者はFで始まる2社くらいではないでしょうか。一方で、実装しようと思えばできるものを「嫌だから変えたい」と言って変えようとするのは、信頼感のある話ではありません。HTTP/2 の仕様策定に関わった当事者が主張するのであれば、なおさらです。

変えるのであれば、皆が納得できるようなものにすべきです。

という考えを踏まえ、今月初頭、僕は Cloudflare 社の Lucas Pardue 氏とともに、HTTP ヘッダベースの優先度制御手法を提案しました(提案仕様, 発表資料)。具体的には、以下のような特徴をもつ提案です。

  • HTTP/2 よりも単純な、各リクエストが独立した優先度を保持する方式
  • たった8種類の優先度
  • サーバとクライアントによる優先度の協調制御
  • HTTP ヘッダを利用することで、優先度制御を HTTP のバージョン非依存にするとともに、将来の拡張性を確保

HTTP/2 方式の堅持を主張するプレーヤーが減ったことで、焦点は、QUIC と HTTP/3 への影響を最小限に抑えつつ、新しい優先度制御手法に如何に軟着陸するか、という点に移りました。

結果、QUIC と HTTP の両ワーキンググループで HTTP/3 から優先度制御に関する規定を取り除くことが決定しました注2QUIC WG 議事録, HTTP WG 議事録)。HTTP/2 についても、現行の優先度制御を廃止するか、「優先度制御しない」ことを表明する機能を追加するかが議論され、後者が採択されました。また、新しい優先度制御手法を議論するサイドミーティングも開かれ、我々の提案した手法を中心に議論が行われました。

今後は、HTTP ワーキンググループにおいて組織された、新しい優先度制御手法を議論する小グループにおいて、迅速な合意が得られるかが焦点になります。

その他、僕の仕事に関係しているところでいうと、共著者の一人を務めている Encrypted SNI は徐々に進展し、また、maprg(測定と解析に関するリサーチグループ)では、Fastly で開発を進めている quicly を使った、衛星ネットワークでの QUICの パフォーマンス試験や、エンドポイントが露出するパケットあたり2ビットの情報を使って、プライバシー問題を抑えつつネットワーク事業者が輻輳の分析を行えるようにする手法の検証報告がありました。

感想:つかれた。

注1: 4月に開催された HTTP Workshop でも話があったのですが、僕は欠席したので雰囲気を知りません。
注2: 優先度を伝達するプロトコルと、その関連規定を取り除くのであって、サーバ独自の優先度制御など、プロトコルによらない手段を否定するものではありません。

Monday, July 22, 2019

pthread_once が嫌いすぎて再実装した話

pthread_once が嫌いです。なぜ嫌いかって言うと、こんな感じで、ファイルレベルのグローバル変数やグローバル関数が出現し、また、値を使う場所と初期化コードの位置が離れがちで可読性が下がるから。

static volatile BIO_METHODS *biom = NULL;

static void init_biom(void)
{
    biom = BIO_meth_new(BIO_TYPE_FD, "h2o_socket");
    BIO_meth_set_write(biom, write_bio);
    BIO_meth_set_read(biom, read_bio);
    BIO_meth_set_puts(biom, puts_bio);
    BIO_meth_set_ctrl(biom, ctrl_bio);
}

static void setup_connection(...)
{
    (いろいろ省略)

    // BIOを初期化
    static pthread_once_t init_biom_once = PTHREAD_ONCE_INIT;
    pthread_once(&init_biom_once, init_biom);
    BIO *bio = BIO_new(biom);
    ...
}


一方、pthread_onceを使う煩雑さを避けようとすると、自前でダブルチェックロックを書くことになるのですが、ダブルチェックロックをちゃんと書くのは難しい(参考:LCK10-J. ダブルチェックロック手法を誤用しない)し、実際間違えるし、毎回、間違えないように書こうとするのはストレスなんです。

というわけで、一念発起して、マクロを使って自分が本当にほしかった「once」を実装しました。

こんな感じで使います。

static void setup_connection(...)
{
    (いろいろ省略)

    // BIOを初期化
    static volatile BIO_METHODS *biom = NULL;
    H2O_MULTITHREAD_ONCE({
        biom = BIO_meth_new(BIO_TYPE_FD, "h2o_socket");
        BIO_meth_set_write(biom, write_bio);
        BIO_meth_set_read(biom, read_bio);
        BIO_meth_set_puts(biom, puts_bio);
        BIO_meth_set_ctrl(biom, ctrl_bio);
    });
    BIO *bio = BIO_new(biom);
    ...
}

グローバル変数やコールバック関数はなくなったし、初期化コードと利用コードが隣同士になって可読性が上がりました。

実際のコードは https://github.com/h2o/h2o/pull/2086 にありますので、ご参照ください。これで正しくダブルチェックロック実装できてるはず。

最後に一言。Cのマクロにブロック渡すのは超便利。

Wednesday, June 12, 2019

#doh_study で Encrypted SNI について話しました

6月11日に開催された #doh_study で登壇する機会をいただきました。

発表では、Encrypt SNI の設計と最近の変更を説明するとともに、背景としてインターネット上のプロトコルに求められる要件がどう変わってきているかを説明しました。



暗号化とプライバシー保護は通信プロトコルの前提条件となりつつありますが、それにともない、企業内の機器やペアレンタルコントロール等において通信内容をどのように管理していくのか。インターネットをより安全で便利なものにするために、ステークホルダーをまたいだ協力が求められています。

参考: Moving control to the endpoints: Motivations, challenges, and the path forward | APNIC Blog

Saturday, September 8, 2018

次世代プロトコル(QUIC etc.)のセキュリティとプライバシー @ #builderscon

9月6日より開催中の builderscon 2018 において、登壇の機会をいただき、インターネットのトランスポート層プロトコルについてセキュリティやプライバシーに関わる設計がどのように進めてられているか、TLS と QUIC を中心に発表しました。

QUIC のハンドシェイクプロトコルとパケット番号暗号化、TLS の Encrypted SNI 拡張は、いずれも僕が提案した機能あるいは方式が採用される予定のものなので、背景にある動機や意義を含め、整理して発表する機会をもらえたことをありがたく感じています。

聴講いただいた方々、また、スライドをご覧になる方々と、次世代プロトコルの暗号応用の手法のみならず意義を含め共有し、理解と議論を深めることができれば、これに勝る喜びはありません。



PS. QUIC のハンドシェイクプロトコルと Encrypted SNI 拡張については、以下のブログ記事もあわせてご覧いただけます。

QUICハンドシェイクの再設計、もしくはTLSレイヤの終焉
TLS の SNI 暗号化に関する Internet Draft を共同提出しました

Tuesday, July 3, 2018

TLS の SNI 暗号化に関する Internet Draft を共同提出しました

Eric Rescorla (RTFM), Nick Sullivan (Cloudflare), Christopher Wood (Apple) の各氏とともに、SNI を暗号化する TLS 拡張を提案する Internet Draft を提出しました。


アナウンスのメールにあるとおり、すでに NSS / Firefox と picotls / H2O で実装作業が開始されており、今月開催される IETF 102 で相互運用試験を行うとともに、標準化にむけた議論を深める予定です。

スノーデン事件以降、広範囲におよぶトラフィックモニタリングによるプライバシー侵害の懸念が明らかになるとともに、できるだけ多くのインターネット上の通信プロトコルを暗号化することが求められるようになってきました (参考: RFC 7258 - Pervasive Monitoring Is an Attack)。

その結果として、ウェブサイトの HTTPS 化が進み、DNS over TLS (RFC 7858)と DNS over HTTPS の標準化により DNS プロトコルの暗号化にも目処が立ち、残る課題は、クライアントがサーバと TLS ハンドシェイクを開始する際に平文で流れるサーバ名注1をどう暗号化するか、という点となっていました。

今回、共同提案した方式は、私が昨年提案した方式を簡略化したもので、ひとことでいうと「木を森の中に隠す」アプローチです。

具体的には、CDN のように、多数のサーバ名を同一の IP アドレスから配信しているという前提注2において、その IP アドレスに属する全てのサーバ名について、同一の ECDH 鍵を DNS を用いて配信し、クライアントはその鍵を利用して TLS ハンドシェイク内のサーバ名を暗号化する、という形になります。

クライアントはサーバのアドレス解決と同時に ECDH 鍵も DNS に問い合わせすることになりますが、HTTP/2 を前提とする DNS over HTTPS では複数の DNS 問い合わせを並列に実行可能なので、接続確立までのレイテンシは従来と変わらないと考えられています。

Firefox が DNS over HTTPS に対応するのに続き、Android が DNS over TLS をサポートするという流れにある今、あとは SNI 暗号化が実装されれば、通信内容を第三者に傍受されたとしても、どのサーバと通信しているのかが漏洩する可能性はとても小さくなります注2

インターネットを利用する人々のプライバシー保護の観点からは前進である一方、海賊版サイトのブロッキングにおいて、ISP レベルでの対応が難しくなることは事実ですが、こちらについては、立法や司法の判断に基づいて配信を停止するという、ユーザのプライバシーを侵害しない形式での手法を検討していただきたいと考える次第です。


注1: TLS では、ひとつの IP アドレスで動作するサーバが、クライアントが指定するサーバ名にあわせて異なるサーバ証明書を配信し、それに基づいて暗号方式を確立します
注2: サーバ名ごとに異なる IP アドレスを使っているケースでは、IP アドレスからどのサーバにアクセスしているか判明するため、サーバ名を暗号化する意味がありません

Thursday, June 14, 2018

Git で全ブランチから検索

ググったけどmacOSでぱっと動くのがなかったのでメモがてら書く。

全ローカルブランチから検索
% git grep keyword $(git branch | colrm 1 2)

リモート含む全ブランチから検索
% git grep keyword $(git branch -a | colrm 1 2)

Tuesday, June 12, 2018

QUICハンドシェイクの再設計、もしくはTLSレイヤの終焉

先週スウェーデンのKistaで開催された第5回QUIC Interimで、ハンドシェイクプロトコルの再設計案の採用が決まりました。

提案者として、その背景にある考え方を整理したいと思います。

▪️提案内容

詳しくはDesign Docを見てもらえばいいとして、ざっくりいうと、
  • TLSスタックをふたつに分割し
  • パケットはQUICがレイアウトしたバイト列をTLSスタックが提供するAPIを使って暗号化注1して生成
  • ハンドシェイクメッセージについては、平文のメッセージをTLSスタックとQUICスタックとの間で交換し、QUICスタック側で上記手法によるパケット化暗号化を行う
というものです。

これにより、たとえばサーバがハンドシェイク時に送出するパケットの構造は以下のようにかわります。

図1. 従来方式

図2. 新方式

赤は難読化(つまり正当なパケットと攻撃との区別がつかない)、黄は未認証の暗号化(通信は暗号化されているが、誰と話しているかはわからない)、青は認証済の暗号化を示しています。従来は5層のレイヤで2重の暗号化をしていたのが、4層のレイヤで1重の暗号化を行うようににかわることがわかります。

なぜ、このような変更を採用することになったのでしょうか。

▪️「レイヤ化」アプローチの限界

伝統的なTLSスタックにおいて、TLS接続はTCP/IP接続の拡張として表現されます。たとえば、JavaのSSLSocketはSocketを拡張したクラスです。

書き込みや読み込み命令はSSLSocketに対して同期命令として発行され、SSLSocketの中で適宜Socket I/Oに変換されます。

この手法の問題は、ソケットの管理が困難になりがちなことです。

たとえば、イベントドリブンなプログラムにおいては、同期I/Oではなく非同期I/Oが求められます。このことは、TLSスタックに非同期I/O用のモードを別途設けるか、あるいは、TLSスタックに対し、アプリケーション側からソケットのようなAPIをもつバッファを提供してやる必要がでてくることを意味します。

また、ヘッドオブラインブロッキングの影響を制御するために、暗号化するブロックサイズを制御し、ブロック単位で送信しようにも、そのような制御を可能にするインターフェイスが存在しないという問題があります注2

これらの点に鑑み、H2Oでは、レイヤ化ではなく、入力バッファと出力バッファを引数としてハンドシェイクや暗号化関数を呼び出すというコーデックスタイルのAPIをもつ独自のTLSスタック「picotls」を開発し、TLS 1.3むけに採用してきました。

▪️「トランスポート層による暗号化」の必要性

TCP上でTLSを使う上での問題点のひとつが、第三者がいつでも接続をリセットできるという点です。このような攻撃が可能なのは、TCP自体が認証付き暗号によって保護されておらず、リセットを引き起こすようなパケットを誰でも生成できてしまうからです。

このようなトランスポート層への攻撃に対処するため、QUICのこれまでのドラフトでは、QUICの上で動くTLSスタックから鍵を「エクスポート」し、この鍵を使ってパケットを暗号化してきました。

この点を指して、picotlsの共同開発者であるChristian Huitema氏は「TLSの利用形態はレイヤからコンポーネントへ変化している」と指摘してきました。プログラミング上の都合のみならず、プロトコル設計上の都合からも、従来のTLSをレイヤとして利用するアプローチが限界に来ていたことがわかります。

利用形態の変化はともかく、これまでのドラフトのアプローチは以下の各点の問題を抱えるものでした。
  • 暗号化をTLSとQUICという2つのソフトウェアスタックで行うという二重暗号化
  • 鍵交換後もハンドシェイク完了まで攻撃に対し脆弱
  • 鍵の使用開始タイミングが不明確

これらの問題を解決しようとする動きとしては、先行してIETF 101におけるEric Rescorla氏によるDTLSへの移行提案がありましたが、DTLSのパケットフォーマットがQUICが必要する機能を提供できない点や、再送制御がハンドシェイク前と後と異なってしまう点などが問題視されていました。

それを踏まえ、DTLSという、QUICとは異なるプロトコルを組み合わせるくらいなら、いっそTLSスタックを分割して、QUICのパケットフォーマットをそのまま使おう、というのが、今回の提案の骨子だったわけです。

従来のドラフトと比較した利点としては、
  • 二重暗号化がなくなる
  • TLS 1.3と同じタイミングで暗号鍵を変更するため、ハンドシェイク中の攻撃耐性が向上する注3
  • 鍵管理をTLSスタックに依存できるようになるので、QUICスタックの品質向上に寄与する
  • パケットフォーマットは引き続きQUICワーキンググループで設計管理、拡張される
といった点があげられることになります。

また、この提案が受け入れられた背景として、TLSスタックの従来のAPIに限界があることが共通理解となっていた、という点もあげられるでしょう。

▪️まとめ

QUICではTLSをハンドシェイクと暗号化という2つの機能に分割して使用することになりました。このことは品質向上につながります。

TLSから見ると、TCP上での使用を前提としたTLS 1.3、UDP上での使用を前提に異なるパケットフォーマットを採用したDTLS 1.3に加え、第3のパケットフォーマットをもつプロトコルが誕生することを意味します。

今後策定されるプロトコルにおいて暗号化が必須であることを考えると、TLSのハンドシェイクメッセージを使いつつ、プロトコル毎に異なるパケットフォーマットを定義する流れが一般化するかもしれません。

長期的にみると、TLSの解体と再構成につながるかもしれませんね。


注1: TLSスタックが共通鍵を提供しQUICスタック側で暗号化を行ってもよい
注2: TLSスタックの下にあるソケットAPIはあくまでバイト列をやりとりするためのものであって、レコードの切れ目がどこにあるかを表現する前提になっていない、ということ
注3: 図2において、赤で示される、パケット注入攻撃が可能なタイミングが短くなっていることが確認できます

Saturday, June 2, 2018

H2O version 2.3.0-beta1 released, improvements presented at Rubykaigi 2018

Today, I am happy to announce the release of H2O version 2.3.0-beta1.

Version 2.3 is going to be the largest release in the history of H2O. Beta-1 already includes more than 50 changes contributed by more than 10 developers.

Improvements include:
  • more powerful mruby handler with Rack and Rack middleware support
  • load balancing in the reverse proxy handler (#1277, #1361)
  • more flexible configuration through the use of !env and stash directives (#1524, 1739)
  • support for new and upcoming HTTP extensions: Server-Timing (#1646, #1717), 103 Early Hints (#1727, #1767), 425 Too Early (#1344)

The improvements related to mruby and HTTP extensions were covered in today's our talk at RubyKaigi 2018 and the slides are below. Please enjoy!

Friday, June 1, 2018

H2O version 2.2.5 released with a vulnerability fix

Today, we have released H2O version 2.2.5.

This is a bug-fix release, including one security-related fix.

The detail of the vulnerability is explained in #1775. Users of H2O are advised to upgrade immediately to version 2.2.5 or to disable access logging.

We would like to thank Marlies Ruck, ForAllSecure for finding the issue.

List of other changes can be found here.

Tuesday, May 1, 2018

IETF報告会で「TLS 1.3とその周辺の標準化動向」について発表してきた

先週金曜日に開催されたIETF報告会にて、「TLS 1.3とその周辺の標準化動向」について発表する機会をいただきました。その際のスライドが下のものになります。



TLS 1.3や関連する提案の技術的特徴とともに、スノーデン事件以来のテーマである通信内容のプライバシー保護とオシフィケーション(硬化)対策がどのように進んでいるか、ご理解いただける一助になれば幸いです。

なお、本スライドは会社のテンプレートを使用していますが、会社としての見解を表明するものではありません。

Thursday, April 26, 2018

海賊版サイトのブロッキングについてアンケートをとってみたら興味深い結果が出た



832票もの回答をいただきました。ありがとうございます。結果をみて、いくつか感想を述べさせていただきたいと思います。

▪️海賊版サイトに対し、なんらかの新たな対策が必要かどうかについて

83%の方々が、なんらかの新しい対策を取ることに積極的賛成、あるいは消極的賛成という立場を取られていることがわかりました。一方で、17%の方々が、少なくとも現時点では新たな対策は不要であり、出版社等の権利者は現行法に基づき、刑事告発、民事訴訟、DMCA Takedownなどの手法を用いて戦うべきだと考えていらっしゃることもわかりました。

▪️ブロッキングという手法について

意見が綺麗に割れました。

42%の方々が、緊急の、あるいは法整備に基づいたブロッキングが適当だとされました。一方、58%の方々が、ブロッキング以外の手法を用いた対策を整備すべき、あるいは整備は不要であると回答されました。
このことからは、「通信の秘密」が単なる法律の条文ではなく、守るべき価値のあるものだと考えていらっしゃる方々が相当数いらっしゃることも読み取れるのかと思います。

▪️ISPが政府の要請に従うべきかについて

5%の方々が、「ISP各社は政府の要請に従ってブロッキングすべき」と答えられました。私のフォロワーには(ブロッキングに従事することに法的リスクがあったり、通信の秘密を重要視したりしがちな)情報通信産業の関係者が多いことを考えると、緊急のブロッキングを求める声は実際にはもっと大きいと考えられます。


皆さんは、この結果を見て、どのように考えられますか?

▪️その他

ツイッターのアンケート機能の、各選択肢が25字制限というのは辛かった。「その他、わからない」という選択肢を足せなかったも悲しかった。

▪️免責事項

私はCDN企業に勤める従業員です(ただし、著作権を侵害するコンテンツの配布を禁止している企業であるため、海賊版サイトとの利害関係はない)。また、IETFにおいて、通信の傍受(と傍受によって可能になるブロッキング)が困難あるいは不可能になるようなプロトコル設計に従事しています。通信の秘密の保護はIETFのプロトコル設計の要件のひとつ(RFC 7258)なのですが、その価値について、他の方々がどのように考えているか、お伺いすることができて、うれしく思っています。

Tuesday, April 17, 2018

Fastlyのプログラマから見たCDN

4月13日に開催されたCDN Study (Akamai/Fastly)で使用したスライドをアップロードしました。Fastlyでミドルウェアを書いているプログラマから見た、CDNの面白さやFastlyの特徴について伝わればいいなと思います。

このエントリーをはてなブックマークに追加

当日は、リクルートさんに会場をご提供いただき、多くの方々、またAkamaiの方々ともと触れ合うことができる、とても貴重な機会となりました。この場を借りて、皆さんに御礼申し上げる次第です。

HTTP/2で 速くなるとき ならないとき

たいへん遅ればせながら、YAPC::Okinawa 2018 ONNNASONで使用したスライドを、こちらにて公開する次第です。

ベンチマークの難しさとチューニングの奥深さ、楽しさを共有できた結果がベストトーク賞につながったのかなと考えています。ありがとうございました&今後ともよろしくお願いいたします。

Thursday, December 28, 2017

CI 用 Docker イメージ作成におけるベストプラクティス

H2O の CI では長らく、秘伝のタレ的な .travis.yml を使い続けてきたのですが、なにぶん依存関係が多いもので、だいぶメンテナンスが辛い感じになってきていました。また、CI テストで発生したエラーの調査の度に、時間のかかる CI を回さなければならないことが、開発者のストレスとなっていました。

そこで一念発起して、Docker イメージを使った CI に切り替えることにしました(実行環境としては引き続き Travis を使います)。

その際に、要件として以下のようなことを考えました。
  • CI以外に、コミット前のテストにも使えるようなイメージにすること
  • コマンド一発でビルドとテストが動作すること
  • 無駄な処理をしないこと

その結果、以下のような実装になりました。
  • テストに必要なソフトウェア群をインストールしたイメージを Docker Hub にアップロードしておく
  • テストには、1. テスト用コンテナにコードのあるディレクトリをマウントし、2. アウトオブトリービルド注1とテストを実行するスクリプトを書く
  • CI環境では、そのスクリプトを実行する
  • 開発者がローカルでそのスクリプトを実行してテストしてもよい

Dockerfile および CI 実行用のスクリプトは、H2O のレポジトリにある以下のファイルです。

結果、Docker 導入前に90行あった .travis.yml は23行にまで短くなり、また、CI に必要な時間も2割ほど減少しました(CI を実行するたびに、必要なソフトウェアをビルドする手間が不要になったため)。

また、開発者が各個人の環境において make -f misc/docker-ci/check.mkmake を実行するだけで、いつでも(コミット前でも)CI 環境と同様のテストが実行できるようになりました。テストが失敗したら、コンテナを立ち上げっぱなしにして、ローカルでコード修正&特定のテスト再実行を繰り返すことも可能なので、バグ修正が捗ります。

まとめ:CI 用の Docker イメージを作るのではなく、テスト用の CI イメージを作り、それを CI に展開すべき

言わずもがなかもしれませんが、備忘録としてまとめておきます。


注1: ソースディレクトリ以外の場所に生成物を配置するビルド手法のこと。アウトオブトリービルドができない場合はaufsを使っても良いのかもしれない