あどけない話

Internet technologies

A new architecture for QUIC in Haskell

In typical UDP programming, unconnected sockets are used both in the client and server sides. sendto() is used to specify a peer address while recvfrom() is utilized to receive a peer address.

In the quic library in Haskell, I used connected sockets for performance reasons. If a connected socket is created for a QUIC connection, recv() can receive data from a specific peer. This means that data is dispatched in the kernel. To understand UDP connection in detail, please read Accepting UDP connections.

As I described in Implementing QUIC in Haskell, I found a drawback of this approach. When a client migrates networks, for instance, from a cell phone network to WiFi, the quic library has to detect the new network interface gets available. It is hard to implement a cross-platform scheme for this detection. So, I added another mode to use a unconnected socket and sendto() for clients.

One of my colleagues told me a drawback of servers recently. The quic library assumes that NAT rebindings do not occur during a connection creation. Once a QUIC connection is created, a server can handle NAT rebidings. What he found is there are NAT boxes which change ports very quickly.

I had to admit that the connected socket approach is not feasible. Therefore, all code for connected sockets were removed and a new approach with unconnected sockets is introduced. Note that sendmsg() and recvmsg() are used instead of sendto() and recvfrom() to work with load balancers of DSR (Direct Server Return).

The quic library version 0.2.0 or later provide this new architecture.

A new architecture for HTTP/2 in Haskell

GHC 9.6 provides listThreads finally. Just for curious, I have implemented a thread monitor in http2-server, a test command tool for the http2 library in Haskell. This revealed that huge numbers of Haskell lightweight threads are used. See the following picture of servers from Experience Report: Developing High Performance HTTP/2 Server in Haskell.

The old architecture makes use of the worker pool. Workers of a fixed number are spawned by the worker manager in advance. A worker takes an HTTP request from the input queue, works for the request, generates an HTTP response and then enqueues the response to the output queue.

Then sender dequeues an HTTP response, fills the output buffer with the available data of the response, flushes the output buffer if necessary, and enqueue the response to the output queue again if it has more data. If the flow control window in the stream level is closed for the HTTP/2 stream, it spawns a waiter thread. The waiter thread wait until the window gets open then enqueues the response again.

For the response of the streaming type, the sender also checks if the streaming data is available. If not, the sender spawns a waiter. The thread monitor revealed that the number of waiters is much larger than I expected. This ruins the saving number of worker threads.

To avoid the thread number explosion, I need to give up the worker pool. Instead, a worker is spawn for an HTTP/2 stream on demand. The sender pushes a response back to the corresponding worker. The worker itself takes care of the flow control window in the stream level or the availability of streaming data.

This new architecture simplified the code drastically:

  • The worker manager is not necessary anymore.
  • The code to go to the next request safely is removed from the worker.
  • The sender should only take care of the flow control window in the connection level.

Version 5.3.0 or later of the http2 library provides this new architecture.

Changes to the paper in Haskell Symposium 2016

  • Client and server code was extracted from Warp to the http2 library. See "HTTP/2 server library in Haskell" and "Implementing HTTP/3 in Haskell" for more information.
  • Section 3 "Priority" is outdated because the priority feature was removed from the HTTP/2 specification. To avoid vulnerability, the priority code was eliminated from the http library.
  • Section 4 "HTTP/2 Implementation in Warp" is also outdated as described in this particle. In particular, Section 4.1 "Optimistic Enqueueing" is meaningless since the number of workers is controlled by SETTINGS_MAX_CONCURRENT_STREAMS (maxConcurrentStreams, default: 64).
  • In Figure 7 and Figure 8 in Section 5 "Evaluation", the stack of Haskell threads consumes large memory. This is because the default value of the stack chunks in GHC (32KiB) is not kind. With this settings, the stack grows like 1KiB, 33KiB, 65KiB, etc. 33KiB is too large as the next step for server usage. Recently, I specify -kc2k to my project code where the step is 1KiB, 3KiB, 5KiB, etc.

I'm a little bit disappointed since I should admit that it's hard for me to write a solid paper about software.

Acknowledgment

I thank Edsko de Vries and Finley McIlwaine for testing the new architecture. They are big contributors to the http2 library in Haskell.

山口でのエギング振り返り

「分かってしまうと、なぜ分からなかったのか分からなくなる」

と言うわけで、いつもは技術的なことしか書かないけれど、今回は「なぜ以前はアオリイカが釣れなかったのか」を忘れない内に書き留めておく。

2022年

親の介護のため、2022年の3月末に、東京から故郷の山口へ引っ越した。瀬戸内の街である故郷では子供達と釣りを楽しもうと思い、「生餌の付けられない子供と一緒にやるにはエギングがいいかな」という軽い気持ちで始めた。

湖畔のキャンプのときに買った2,000円ぐらいのバスロッドを使い、格安のエギを買って5月ごろにエギングをやってみたが、まったく釣れなかった。今思えば当たり前だけど。釣れないので子供達は飽きてしまい、エギングには付き合ってくれなくなった。

秋になり、娘の習い事を待つ間に、近くの漁港に行って、20分だけエギングをやった。しかし、何度やっても釣れない。言い訳できないように、すごく釣れると言うエギ王K ムラムラチェリー2.5号を買い、「今日釣れなかったらやめる」と言う気持ちで、明かりの消えた漁港で竿を振った。10月6日の夜のことだ。

「これがラスト」と思って投げたエギを、ただ巻きしていると急に重くなった。初めはゴミがかかったのか思ったが、何か違う。足元でヘッドライトを当ててみるとアオリイカで、無造作に引き上げた。初めて釣れたというのに、当たりが取れなかったせいか、あまり嬉しくはなかった。

よくやめなかったと思う。このアオリは、エギングの神様が「エギングをやめるな」と与えた1杯だったのかもしれない。

2023年

介護や子供の世話が忙しく、あまり記憶がないのだけれど、春はエギングをやならなかったと思う。このころ、「ウチの近くにはアオリイカなんて、ほとんどいないんだ」と思い込んでいた。釣れると聞いたエギ王K 軍艦グリーンも、まったく市場に出回ってなかった。

9月24日の親戚の集まりで、Tさんから「俺は港の藻がある方側で2杯釣ったぜ」と言われ、少しやる気が出て、また待ち時間の間に行ってみた。下手過ぎて、すぐにエギを根掛かりさせてしまうので、ジュンテンドーで買った安いエギを単にただ巻きしていた。これまた、「これで帰る」と思って投げた一投で、生命感のあるピリピリとした当たりがあり、人生2杯目のアオリイカが釣れた。驚くべきことに、写真を撮っておらず、何日のことだったかはもう分からない。

この秋3杯釣ったら自分へのご褒美で、エギングロッドとリールを買うと決めていたが、釣具のポイントの店長さんと話していると、今すぐ欲しくなって、エントリーモデルを買ってしまった。竿は色々調べて決めていた「シマノ セフィア BB S83ML」、リールは店長の合わせた方がいいと言うススメに従い「シマノ セフィア BB C3000S DH」にした。

この後、フリーリグでチヌを釣ることに時間を使ってしまったので、2023年も釣れたのは秋イカ1杯だけだった。

2024年5月

4月に父を送り出し、雑多な手続きもこなして落ち着き始めた頃、またTさんから「今年のアオリはデカいらしい。俺は釣ってないけど」と言われ、夜にポイントを回ってみた。あるポイントには驚きの光景が広がっていた。たくさんの車が停まっており、椅子が丸く並べられていて、キャンプファイヤー状態。地面には、これまで見たこともない大きなイカが、「釣れるのが当たり前」みたいな感じで、あちらこちらに無造作に置かれていた。このとき初めて、自分の故郷に春イカの一級ポイントがあることを知った。

イカは自分の近くにいるし、竿とリールには投資したし、エギ王K 軍艦グリーンの在庫も復活した。これで釣れないのは、自分の腕が悪からだと素直に信じれるようになり、余裕が出てきた時間を使って、足繁くポイントに通った。そしてとうとう5月29日、春の大きなイカを釣り上げた。モンゴウだけど。

引き上げても横抱きしたエギを離さず、エギに噛み跡を残してくれた君には感謝しかない。モンゴウは細く切らないと美味しくないことを知らずに、息子には「美味しくない」と言われてしまった。まずかったのは、僕の調理の腕だ。許してくれ。

僕にも大きなイカが釣れることが分かり、火がついた。どうしても春のアオリイカが釣りたい。上手な人たちに混じって初心者の自分が釣るには、違うことをするしかないと思い、youtubeで見たスローエギング(リフト&フォールみたいな感じ)をやっみることにした。

次の日の夜、よい場所に入れたので、そこだけ波が立っている場所に軍艦グリーン 3.5号 シャローを投げ、ゆっくりゆっくり誘っていると、もわーん、もわーんとした生命反応を感じた。どう合わせていいのか分からず、乗ったのかどうかも分からないので、我慢してゆっくりゆっくりエギを動かし、「もう逃げたかな」と思って巻き始めたら、急に重くなり一回だけ走った。

水面に現れた姿を周りの人が見付けて「アオリだ!」と言う。かなり近づいたとき、ようやく僕にも見えた。「触腕一本だから気を付けて」と言われるがどうしていいのか分からない。周りの上手い方に、「横に滑らすように」と教えてもらい、何度もやり直して、ようやくタモ入れできる体制に持ち込んで、タモ入れしてもらった。

苦節2年を経た2024年5月30日、ついに春のアオリイカ(オス 926g)を釣り上げた。本当に嬉しかった。

2024年6月

エギング歴の長い方とたまたまポイントで2人きりになったときのこと。僕のしゃくり方がおかしいと指摘していただたいた。「ショートジャークをしているつもりだろうけど、ハンドルを回しているから、エギはまっすぐにしか動かず、すぐに戻って来てしまう。竿を回すようにすると、エギは斜め前方に動くから、岸に向かっての移動距離は短くなり、イカを多く誘える」と教えていただいた。ショートジャークはエギをダートさせるため、つまり横に動かすのが目的だとまったく分かっていなかった。

6月も半ばを過ぎると、多くの人がまったく釣れなくなり、釣り人の数も少なくなった。僕は大潮にはまだアオリが入ってくるだろうと信じ、気になっていたパタパタを買って、6月19日の夕刻に海へ向かった。すると、奇跡的に一投目で釣れた。安定のしゃくり合わせ。両隣の人も、僕の真似をしてパタパタに変えると釣れて3連チャン。時合だったのだ。1172gと夢のキロアップ! メスを持ち帰ってはいけないというルールはないそうで、心に余裕のない僕は持って帰ることにした。(クワガタのメスは取らないよ。本当だよ!)

僕が行くポイントには、みんなが釣れてない中で必ず釣って帰る人がいる。その人に話しかけてみると、「エギング歴25年で、ほとんどボウズはない」と言う。その人の隣で竿を振りながらいろいろ教えてもらった。「君のエギはダートはしているが、跳ね上がっていない」と言う。「2段しゃくりと言う言葉は古いのかもしれないけれど、上に動かさないとイカのスイッチが入らない」そうだ。「潮が動いているかはゴミの位置を観察したら分かる」。「エギはプロスペックの薩摩オレンジで、ローテーションはほとんどしない」。「フケ当たりっていうのは、こんな感じ」と実演してくださった。

そのときから、エギを動かすときに、跳ね上げとダート、つまり縦の動きと横の動きを意識するようになった。すると、その日の内に1375gの大物が釣れた。モンゴウだけどね。6月29日、エギはエギ王K 金アジ 3.0号。

2024年7月

7月4日にまた同じエギでモンゴウが釣れた。この日は自分しかいなかったので、初めて一人でタモ入れしたけど、イメージトレーニングはばっちりで、手際よく引き上げた。計量を忘れしまったので、重さは分からないが、中ぐらい。

そして冒頭の呟きをしたわけだ。次の日にしゃくり合わせでかかったイカは、走る走る。ドラグが鳴り止まず、かなり期待したのだけれど、浮き上がって来たのは873gのモンゴウだった。エギは、通販で手に入れたプロスペック 薩摩オレンジ 3.5号!

どうしてもアオリの3杯目を釣りたい。僕に残された手は朝マズメのみ。夜の家事を早めに済ませ22時には就寝して、7月8日は4時に起きて海に向かう。最初はラインが見えない程暗かったが、明るくなってきた。長雨のせいで潮は濁っているから、底は澄んでいると願って薩摩オレンジで底を攻める。すると、ラインが一瞬フケるのが見え、間髪入れずに合わせると乗った!396gと小さいが、目標の3杯達成!

同日、高校の同級生から「今日は呑み会の日だ」と連絡があり、「前回日程を決めてなかったんだから知らんがな」と思いつつ、子供の送迎もないので、アオリとモンゴウのサクを店に持って行った。僕はアオリを自慢しに来ているのに、みな口を揃えて「モンゴウがうまい」と言う。同席していた親戚のRさんから「モンゴウをもう1杯釣れ」と指令が下った。あの方向に金アジを投げて底を攻めれば釣れると分かっていたので、「釣りますよ」と安請け合いをする。

寝不足なので次の日は普通の時間まで寝ると決めていたのに、4時に目が覚め雨は降っていない。エギングの神様に「海に行け」と言われた気がしたので、昨日と同じようにポイントに向かう。ノルマのモンゴウを釣るために、金アジを投げてしゃくるが、ラインが見えずしっくりこないので、回収を始めた。するとエギの後をロケットのように追ってくるイカがいて、ヘッドライトを当てたのに逃げない。慌ててエギを止めてフォールさせると抱いた!1296gのモンゴウに人生初のサイト・エギングをきめてノルマを達成。

墨跡を掃除しているとラインが見える程度に明るくなってきた。時合だと感じ、エギを薩摩オレンジに変えて底を攻める。風があるのでやりずらいが、ラインが一瞬だけフケたのを見逃さなかった。1089gとキロアップのアオリ! 1日2杯釣れるなんて夢のようだ。

釣り人も少なくなって快適だし、(ここがではなく、一般論では)夏もアオリが釣れないことはないらしいので、7月いっぱいはエギングを続けて、8月は休み、9月から秋イカを狙おうと考えている。

なぜ釣れなかったのか

まぁ結局、エギングに対する態度も中途半端だったし、何も分かっていなかった。

  • エギングにかける時間が短過ぎた → 時間をかけるしかありません。教えてくれる人に出会えると素敵ですね
  • ロッドやリールにお金をケチった → 3万円ぐらい出して、エントリーモデルを買いましょう。安い竿でエギングをやるのは時間の無駄です
  • ロストばかりするので、高いエギを買わなかった → 鉄板の「エギ王K」とか「パタパタ」とかを買いましょう。道具の中でエギが一番大切です
  • ロストを回避する方法と根掛かりしたときに回収する方法が分かってなかった → 底に着いたときの最初は強くしゃくるのをやめましょう。藻にかかったら、竿を斜め上に片手で細かくしゃくり続けると外れます
  • 自分の近くにはアオリイカはあんまりいないと間違った認識をしていた → 瀬戸内にもアオリイカはいます。日本海側に行く必要はありません
  • 縦の動きである「跳ね上げ」と横の動きである「ダート」の違いも分かっていなかったし、認識していなかったから動かし方も知らなかった → 「ワンピッチ・ショート・ジャーク」と「2段しゃくり」を覚えましょう
  • 潮が動くという意味も分かってなかった → ゴミを観察して短時間で移動したら流れてます。エギにかかる抵抗で感じることもできます
  • どの潮のどの潮位のときに釣れるか分かっていなかった → 生意気ですが、企業秘密とします
  • 当たりの取り方が分かっていなかった → 僕も夜だとよく分からないので、ラインが見える時間帯にやって、フケ当たりを見逃さないようにしましょう。底に着いたのか、フケ当たりなのか分からなくても、とにかくフケたら合わせるのです。夜でも運がよければ、生命感のある当たりも来るかもれません

最後に、あるロッククライマーが言った言葉で、このブログを締めくくることにする。

「長く続けていれば、いいことあるよ」

Happy Eging!

Status report of dnsext

This article reports the current status of the dnsext packages in Haskell. If you don't know what dnsext is, please read "Developing network related libraries in Haskell in 2022FY" first. The purpose of this project is provide DNS full resolver (cache server).

bowline

Our DNS full resolver is now called bowline named after the king of knots. (I used to climb rocks with double eight knot but I like bowline.) Most importantly, our DNSSEC verifier has been integrated into bowline by Kei Hibino.

New features

To make bowline practical, the following features have been added:

  • Configuration: bowline.conf is in the key-value format. Especially, local-zone:, local-data:, root-hints: and trust-anchor-file: can be specified.
  • Logging: the standard way of logging for DNS servers is DNSTAP whose format is protocol buffer and transport is fast stream. Instead of using other libraries, we implement them in dnsext-utils.
  • Statistic monitoring: Prometheus is supported.
  • Web API: the recent trend for server management is containers. When servers run in containers, the traditional signal scheme is not feasible. So, bowline provides web API for reading statistic, reloading, etc.

DNS transport

To protect privacy, the transport between DNS full resolvers and stub resolvers should be encrypted. The send-receive API of tls and quic is suitable to implement DoT (DNS over TLS) and DoQ (DNS over QUIC). However, the worker model of http2 and http3 is inefficient for DoH (DNS over HTTP). To emulate the send-receive API, runIO is implemented and provided from Internal module of http2. Unfortunately, I have no idea on how to implement runIO for http3 at this moment.

While verifying safety of http2 and quic, I noticed that not all cases of flow control are covered. The following should be implemented for stream numbers in a connection, amount of sending/receiving data in a connection and amount of sending/receiving data in a stream:

  • Telling the limit of receiving data to the peer in proper timing
  • Closing the connection if the receiving data reaches the limit
  • Sending data with the respect of the limit of sending data

To extract common patterns of flow-control, the network-control package is created. With network-control, http2 and quic have covered the all cases.

Refactoring and testing

The code for iterative queries was huge and monolithic. So, it was divided into multiple modules with the help of calligraphy which can visualize call-graph of functions.

dnsperf is used to measure server performance and to run stress testing. We noticed that stacks of Haskell lightweight threads consume huge memory. Their initial size of 1 KiB. When the limit are reached, they glow 33 KiB since the next chunk size is default to 32 KiB. In my opinion, this value is too big because threads might use only 2 KiB, for instance. So, we specify -kc2k (2 KiB) as an RTS option so that the size of stack glows 1KiB, 3 KiB, 5 KiB, 7 KiB and so on.

dug

dug is a command line interface for DNS queries. Of course, it can resolve records for a target domain using UDP as a stub resolver:

% dug www.iij.ad.jp aaaa
;; 2001:a7ff:5f01:1::a#53/UDP, Tx:42bytes, Rx:196bytes, 34usec
...
;; ANSWER SECTION:
www.iij.ad.jp.  300(5 mins) IN  AAAA    2001:240:bb81::10:180

The characteristics of dug are as follows:

  • Queries can be sent with DoT, DoQ and DoH if the -d option is specified.
  • Such a transport is automatically selected by parsing SVCB RRs if the -d auto is specified.
  • It can execute the iterative query algorithm used in bowline if the -i option is specified.

The followings are the new feature added in 2023FY:

  • tcp is added in addition to auto, doq, dot etc for the -d option.
  • The result of DNSSEC is displayed with colors if the --demo option is specified.
  • The query result is displayed in the JSON format if the --json option is specified.

Releasing tls library version 2.0.0 in Haskell

I needed to implement the session ticket mechanism for my project. In addition to this coding, I decided to improve the tls library in Haskell drastically. So, I have spent three months to do so and finally released tls vresion 2.0.0. This version is secure by default and its code readability is improved. This article explains what changed.

Removing insecure stuff

tls version 1.9.0 supports TLS 1.0 and TLS 1.1 in addition to TLS 1.2 and TLS 1.3. RFC 8996 deprecates TLS 1.0 and TLS 1.1. So, they are removed from tls.

TLS 1.2 is considered secure if configured correctly while TLS 1.3 is considered secure by design. To ensure secure configuration, the followings are removed according to "Deprecating Obsolete Key Exchange Methods in TLS 1.2":

  • CBC ciphers
  • RC4 and 3DES
  • DSS(digital signature standard)

The current cipher suites for TLS 1.2 are only:

  • TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
  • TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
  • TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
  • TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
  • TLS_ECDHE_ECDSA_WITH_AES_128_CCM
  • TLS_ECDHE_ECDSA_WITH_AES_256_CCM
  • TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8
  • TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8
  • TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
  • TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256

To prevent the triple handshake attack, the extended main secret defined by RFC7627 is necessary. supportedExtendedMasterSec was default to AllowEMS. It is renamed to supportedExtendedMainSecret and its default value is set to RequireEMS.

I believe that your code can be built without any modifications if you don't customize parameters heavily. If your code cannot be built, I'm sorry, but this breaking changes are intentional to tell that you are using insure parameters for TLS 1.2.

Catching up RFC8446bis

TLS 1.3 is defined in RFC8466 and it is being revised in RFC8466bis. Important changes are:

  • The word "master" is renamed to "main".
  • general_error alert is defined.

tls v2.0.0 catches up RFC8466 bis as much as possible.

Improving API

To send early data in TLS 1.3, clientEarlyData should be used in tls version 1.9.0. Fixed string can be passed through this interface but it is not feasible for real usage since applications decide early data dynamically. With tls version 2.0.0, sendData can now send early data if clientUseEarlyData is set to True.

Client's handshake for TLS 1.3 can now receive the alert of client authentication failure.

Client's bye can now receive NewSessionTicket in TLS 1.3.

Refactoring

handshake was monolithic. To follow the handshake diagram of TLS 1.2 and 1.3, its internal is divided. The result code for TLS 1.2 client looks:

handshake cparams ctx groups mparams = do
    ...
    crand <- sendClientHello cparams ctx groups mparams pskinfo
    ...
    (ver, hss, hrr) <- receiveServerHello cparams ctx mparams
    ...
    case ver of
        TLS13 -> ...
        _ -> do
            recvServerFirstFlight12 cparams ctx hss
            sendClientSecondFlight12 cparams ctx
            recvServerSecondFlight12 ctx

The test framework is switched from tasty to hspec. The quality of each test case is improved.

Also, the following modifications are done:

  • The code is now formatted with fourmolu.
  • PatternSynonyms is introduced for extensibility.
  • The Strict and StritData pragma are specified.

Session Manager

tls 1.9.0 has an abstraction for session management called SessionManager:

data SessionManager {
    sessionResume :: SessionID -> IO (Maybe SessionData)
  , sessionResumeOnlyOnce :: SessionID -> IO (Maybe SessionData)
  , sessionEstablish :: SessionID -> SessionData -> IO ()
  , sessionInvalidate :: SessionID -> IO ()
}

Network.TLS.SessionManager in tls-session-manager version 0.0.4 provides SessionManager for in-memory session DB. When implementing the session ticket mechanism, it appeared that this abstraction is not good enough since there are no way to return tickets. So, SessionManager in tls version 2.0.0 is now:

data SessionManager {
    sessionResume :: SessionID -> IO (Maybe SessionData)
  , sessionResumeOnlyOnce :: SessionID -> IO (Maybe SessionData)
  , sessionEstablish :: SessionID -> SessionData -> IO (Myabe Ticket)
  , sessionInvalidate :: SessionID -> IO ()
  , sessionUseTicket :: Bool
}

Network.TLS.SessionTicket is finally implemented in version 0.0.5.

Interoperability test

To test interoperability with other implementation, I use tls-simpleclient and tls-simpleserver in tls-debug. Unfortunately, I don't have the upload permission for tls-debug to Hackage. Also, it's very inconvenient to build them since they are in the separate package. So, I imported them into the util directory of tls and renamed to client and server. To build them, specify the devel flag to cabal or your favorite command.

client and server are tested with OpenSSL and gnutls both for TLS 1.2 and 1.3.

APIから見たTLS 1.2の同期性と1.3の非同期性

プロトコル自体を比べるとTLS 1.2よりもTLS 1.3の方が簡潔で洗練されている。しかし、APIの視点から見ると、TLS 1.3は非同期性が高くなる。同期性的なTLS 1.2のプロトコル設計には、妥当性があると気づいたので記録を残す。

以下のような API を考える

  • new -- コンフィグを与えるとコンテキストを返す
  • handshake -- コンテキストを与えると、ハンドシェイクを試みてTLSコネクションを作成する
  • sendData -- コンテキストとデータを与えると、データを送信する
  • recvData -- コンテキストを与えると、データを受信する
  • bye -- アラートを送って、TLSコネクションを終了する

クライアントの疑似コードは、こんな感じ:

ctx = new(コンフィグ);
handshake(ctx);
sendData(ctx, "Hello world!");
rc = recvData(ctx);
bye(ctx);

クライアント認証とNewSessionTicket

TLS 1.2のフルハンドシェイクでは、クライアントがサーバからFinishedを受け取って終了する。

         Client                                               Server

handshake: ClientHello
           (empty SessionTicket extension)-------->                  : handshake
                                                          ServerHello
                                      (empty SessionTicket extension)
                                                         Certificate*
                                                   ServerKeyExchange*
                                                  CertificateRequest*
                                       <--------      ServerHelloDone
           Certificate*
           ClientKeyExchange
           CertificateVerify*
           [ChangeCipherSpec]
           Finished                    -------->
                                                     NewSessionTicket
                                                   [ChangeCipherSpec]
                                       <--------             Finished
sendData:  Application Data            -------->                     : recvData
recvData:                              <--------     Application Data: sendData
bye :  Alert                       -------->

TLS 1.2 のクライアントのhandshakeは、サーバのFinishedを待てるから:

  • クライアントの handshake は NewSessionTicket を受け取れる
  • クライアントの handshake はクライアント認証が失敗した際、Alert を受け取れる

クライアント認証が必要なくなるセッションの再開では、クライアントが Finished を送ることでハンドシェイクが完了する。

         Client                                                Server
handshake: ClientHello
           (SessionTicket extension)    -------->                    : handshake
                                                          ServerHello
                                      (empty SessionTicket extension)
                                                     NewSessionTicket
                                                   [ChangeCipherSpec]
                                       <--------             Finished
           [ChangeCipherSpec]
           Finished                    -------->
sendData:  Application Data            -------->                     : recvData
recvData:                              <--------     Application Data: sendData
bye     :  Alert                       -------->

実に同期的だ。非同期な要素としては、recvDataが正常終了や異常終了のアラートを受け取るぐらいである。

一方、TLS 1.3のハンドシェイクは以下の通り:

       Client                                           Server

handshake: ClientHello
           + key_share*
           + signature_algorithms*
           + psk_key_exchange_modes*
           + pre_shared_key*    -------->
                                                  ServerHello: handshake
                                                 + key_share*
                                            + pre_shared_key*
                                          EncryptedExtensions
                                          CertificateRequest*
                                                 Certificate*
                                           CertificateVerify*
                              <---------             Finished
           Certificate*
           CertificateVerify*
           Finished            -------->
sendData:  Application Data    -------->                     : recvData
recvData:                      <--------    Application Data : sendData
                                            NewSessionTicket
bye:       Alert               -------->

クライアントの handshake は、Finishedを送って終わるので、NewSessionTicketを受け取れない。このため、recvDataやbyeにNewSessionTicketを受け取る非同期的な工夫が必要となる。

同様にクライアントのhandshakeは、普通に実装したら、クライアント認証が失敗した際のアラートを受け取れない。handshakeがエラーを返さずに戻ってきたのに、recvDataがクライアント認証失敗のアラートを受け取るようなAPIは、とても使いずらい。よって、クライアントhandshakeがCertificateを送った場合は、一定期間アラートが戻ってこないか見張るべきだ。その間に、通常のデータを受信したら保存しておき、次にrecvDataが呼ばれた際に、そのデータを返すべきである。

余談だが、TLSレベルのクライアント認証は何かと問題が多いので、HTTPのレベルで証明書を使ったクライアント認証を実現しようという動きがある。

0-RTT

TLS 1.3 の 0-RTT は、とても非同期的だ。クライアントが、0-RTTでearly dataを送る場合、専用のAPIを与えると実用に乏しいことが経験的に分かった。そこで、これまでのsendDataでearly dataを送ることを考える。

handshake: ClientHello
           + early_data
           + key_share*
           + psk_key_exchange_modes
           + pre_shared_key
sendData:  Application Data    -------->
                                                  ServerHello: handshake
                                             + pre_shared_key
                                                 + key_share*
                                          EncryptedExtensions
                                                + early_data*
                               <--------             Finished
sendData:  EndOfEarlyData
           Finished           
           Application Data    -------->                     : recvData
recvData:                      <--------    Application Data : sendData
                                            NewSessionTicket
bye:       Alert               -------->
  • クライアントのhandshakeは、ClientHelloを送ったら戻ってこないといけない。フルハンドシェイクのときは、戻らないことに注意。
  • クライアントのsendDataは、ハンドシェクが完了したら EndOfEarlyData と Finished を送らないといけない
  • サーバのrecvDataは、EndOfEarlyData と Finishedも受け取らないといけない

という訳で、TLS 1.3のAPIは大変なのだ。

私とテストと自動化と

何度か講演でこの話をしたのだが、気が向いたのでエッセンスを書き下しておこうと思う。

テスト駆動という言葉が流行る前にプログラマとなった私は、当初どのようにテストを書いてよいのか分からなかった。そんなとき、(当時はオーム社で現在はラムダノートの)鹿野さんから「ビューティフルコード」を献本していただいた。分厚い本なので、興味ある章から読んでいった。その一つがアルベルト・サボイア氏が書いた7章「ビューティフル・テスト」だ。

この章では、例として二分探索が取り上げられる。二分探索のアイディアが出されたのは1946年だが、バグのない実装ができたのは12年後だという。実際に実装してみると分かるが、ソートされた配列の中に目的の要素が含まれるのか検査する二分探索のコードを正しく書くことは難しい。格好の題材というわけだ。

テストがよく分かっていない私にとってありがたかったのは、JUnitの設計指針が説明されていたことだ。assertEqualsで単純に比較できるようになっており、この関数を使ってテストできるようにコードを設計しないといけないと分かった。

この章では以下のようなテストが説明される。

スモークテスト

単純な利用例を使ったテストである。(この書籍には書かれてないが)名前の由来は、昔、ハードウェアが完成したら、まず電源を入れて煙が出ないか確かめたことだという。

実際には、次のように書く。(適当な疑似コードなので、雰囲気で理解して欲しい。)

# 対象となる配列
arr = {1, 4, 42, 55, 67, 87, 100, 245}
# 42 が入っている位置を確認
assertEquals( 2, binarySearch(arr, 42))
# 43 が入ってないことを確認
assertEquals(-1, binarySearch(arr, 43))

境界テスト

境界テストとは、データ構造の端っこを検査することである。固定長の数値あれば、下限と上限のテストは簡単である。配列のようにいくらでも大きくなれるデータ構造であれば、少なくとも大きさ0や1といった下限をテストすべきである。二分探索では、配列が半分、半分と分割され境界が変わっていくので、境界テストが大切になってくる。

配列の下限のテストは、以下のように書ける。

# 大きさ0の配列
assertEquals(-1, binarySearch({    }, 42))
# 大きさ1の配列
assertEquals( 0, binarySearch({ 42 }, 42))
assertEquals(-1, binarySearch({ 42 }, 43))

以下は、要素の位置に関する境界テストである。

arr = { -324, -3, -1, 0, 42, 99, 101 }
# 左端
assertEquals(0, binarySearch(arr, -324))
# 真ん中
assertEquals(3, binarySearch(arr, 0))
# 右端
assertEquals(6, binarySearch(arr, 101))

世間では、こういったテストをCIなどで自動的に走らせることを「テストの自動化」と言っている場合が多い。

ランダムテスト

境界テストを書いていると、いろんなテストデータを人間が用意するのは馬鹿らしいと思うようになる。テストデータは自動生成したい。ランダムテストとは、テストデータを乱数的に生成するテストのことである。

テストデータをうまく生成できれば、コーナーケースを発見しやすくなる。個人的には、ここまでの自動化をやって初めて「テストの自動化」と呼びたい。

突然変異テスト

テストするコードに若干変更を加えて、テストする方法である。若干の変更が加わった突然変異は、元のコードとは若干異なる振る舞いをする。テストが十分であれば、突然変異はテストをすり抜けられない。言い換えると、突然変異がテストをすり抜けたとしたら、テストは十分でないので、テスト項目を増やす必要がある。突然変異テスト対しては深入りしないので、興味があればビューティフルコードを読んで欲しい。

私はビューティフル・テストの章を読んで景色が変わった。テストが書けるようになったのだ。テストのためには、コード側もテストしやすいように作る必要がある。テストのし易さが、私の設計指針に加わったのだ。

性質テスト

Haskellでプログラムを書くようになり、Haskellerの常としてQuickCheckと出会った。QuickCheckとは性質テストのための代表的なテストフレームワークである。コードの性質を書いておくと、テストデータは自動生成される。

乱数テストの一種とも言えるが、テストデータの生成に工夫がある。テストデータは、小さいものから、だんだん大きくなっていくので、下限の境界が網羅される。また、たとえば、木構造に対して深さ優先で生成するのか、幅優先で生成するのかといった戦略がテストフレームワークごとに決まっている。

QuickCheckを使って性質テストを書くようになってからしばらくして、ビューティフル・テストを読み返してみた。以前あれほど感動した内容なのに、「なにをまどろっこしいことをやっているのだ」という感想に変わったのだ。

スモークテスト、境界テスト、ランダムテスト、そして突然変異テストで説明してあることは、計算量を除いて、単に線形探索と同じ振る舞いをするというだけのことだ。つまり、性質テストであれば以下の一行で書ける。(本当だよ。)

# == は単に等しいかを調べる
# arr や x は自動生成される
linearSearch(arr, x) == binarySearch(arr, x)

なんということだろう。気付かないうちに、また景色が変わっていたのだ。

私はHaskellTLSライブラリを保守しているが、テストにはQuickCheckを使った性質テストが書かれている。クライアントとサーバに渡すパラメータが自動生成され、共通のパラメータがあればハンドシェイクは成功し、なければ失敗するといった具合だ。

Happy testing!