あどけない話

Internet technologies

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!

セッション再開に関するTLS1.2と1.3の違い

これまでTLS 1.3とセッションID方式は実装したことがあったが、この経験だけではTLS 1.2に対するTLS 1.3の利点に気づいていなかった。この二ヶ月の間に、セッションチケット方式を実装し、また引き継いだTLS 1.2のコードを大幅にリファクタリングした過程で、セッションの再開は、TLS 1.2よりもTLS 1.3の方が安全であると気づくことができた。備忘録として安全である理由を書いておく。

以下は、セッションID方式にもセッションチケット方式にも、共通している性質である。説明の都合で、セッションチケット方式を取り上げる。

TLS 1.2

TLS 1.2用のセッションチケット方式は、RFC 5077で定義されている。以下にRFC 5077からTLS 1.2のフルハンドシェイクの図を抜粋する。

         Client                                               Server

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

NewSesionTicketはChangeCipherSpecの前に送られる。つまり、(セッションチケット自体はサーバしか知らない秘密鍵で暗号化されているが)通信自体は平文である。(ChangeCipherSpecが角括弧で囲まれているのは、ハンドシェイクメッセージではないという意味。)

サーバ認証を省略し、TLS 1.2ではメインシークレットを復元するための仕組みがセッションの再開である。再開の図も引用する:

         Client                                                Server
         ClientHello
         (SessionTicket extension)      -------->
                                                          ServerHello
                                      (empty SessionTicket extension)
                                                     NewSessionTicket
                                                   [ChangeCipherSpec]
                                       <--------             Finished
         [ChangeCipherSpec]
         Finished                      -------->
         Application Data              <------->     Application Data

フルハンドシェイクではクライントが先に(ハンドシェイクメッセージ全体のチェクサムである)Finishedを送るが、再開ではサーバが先に送る。

サーバは、再開時に再びNewSessionTicketを送ってもよい(MAY)。しかし、メインシークレットを変える方法はないので、同じメインシークレットを格納し、単に使用期間が伸びたチケットを発行することになる。OpenSSLのs_serverで試してみると、再開時にはNewSessionTicketを送ってこない。

このようにTLS 1.2では、セッションの再開を使うと、サーバ認証を省略できるものの、メインシークレットを更新する方法がなく、メインシークレットを複数のセッションで使い回すことになる。

TLS 1.3

TLS 1.3のフルハンドシェイクをRFC 8446から引用する:

          Client                                               Server

   Initial Handshake:
          ClientHello
          + key_share               -------->
                                                          ServerHello
                                                          + key_share
                                                {EncryptedExtensions}
                                                {CertificateRequest*}
                                                       {Certificate*}
                                                 {CertificateVerify*}
                                                           {Finished}
                                    <--------     [Application Data*]
          {Certificate*}
          {CertificateVerify*}
          {Finished}                -------->
                                    <--------      [NewSessionTicket]
          [Application Data]        <------->      [Application Data]

角括弧で囲まれたメッセージは、暗号化されていることを表しているので、NewSessionTicketは暗号化されて送信されことが分かる。

次にセッションの再開の図も引用するが、説明の都合上NewSessionTicketを書き加える:

   Subsequent Handshake:
          ClientHello
          + key_share*
          + pre_shared_key          -------->
                                                          ServerHello
                                                     + pre_shared_key
                                                         + key_share*
                                                {EncryptedExtensions}
                                                           {Finished}
                                    <--------     [Application Data*]
          {Finished}                -------->
                                    <--------      [NewSessionTicket]
          [Application Data]        <------->      [Application Data]

このセッションでの(複数の)メインシークレットは、pre_shared_keyに含まれる(前のセッションで受け取った)セッションチケットの情報と、key_shareを利用して生成された一時的な鍵から生成される。端的に言えば、前回のセッションとは異なるメインシークレットが生成される。よって、新たに発行するNewSessionTicketにも、新しいメインシークレットが格納される。このように、TLS 1.3では、各セッションで独自のメインシークレットが使用される。

まとめ

セッションの再開に関して、TLS 1.2よりもTLS 1.3の方が安全であると言う根拠をまとめると以下の通り:

  • TLS 1.2のNewSessionTicketは平文の通信路で送られるが、TLS 1.3では暗号路で送られる。
  • TLS 1.2では複数のセッション間で同一のメインシークレットを使うが、TLS 1.3ではセッションに固有のメインシークレットが使われる (重要)

2023年にHaskell関連で知ってよかったこと

これはHaskell Advent Calendar 2023の19番目の記事です。

フォーマッター

以前、フォーマッターをいくつか試しましたが、どれもイマイチでした。しかし、fourmoluはいけてます。fourmoluは、Ormoluのフォークで、Ormoluが偉大なのでしょう。両方試しましたが、僕はformoluに決めました。

Hackageに上がっているので好きな方法でインストールしてください。

% cabal install fourmolu

formoluにHaskellのプログラムを渡すと、整形したプログラムを出力してくれます。ファイルの内容を直接書き換えたいときは、-iオプションを渡します。エディタやIDEと連動できますが、お試しでプロジェクト全体を整形するには、以下のようにするといいでしょう。

% find . -name "*.hs" | xargs fourmolu -i

整形が気に入らない部分は、{- FOURMOLU_DISABLE -}{- FOURMOLU_ENABLE -}で囲んで、手で修正してください。

CPPで #ifdef している部分は、書き方によって、整形できたり、できなかったりします(ファイル全体の整形を諦める)。関数の一部ではなく、冗長になるかもしれませんが関数の全体を#ifdefで囲むように変形すると、きっとうまく整形できるでしょう。

整形の方法をfourmolu.yamlでカスタマイズできます。fourmoluは、起動時にルートディレクトリに向かって、このファイルを探します。設定の各種項目を試せるWebサイトがあるので、そこで遊んでみましょう。僕が使っている設定ファイルは、ここを見てください。

コールグラフ

いつの間にか巨大になってしまったモジュールを分割するとき、コールグラフがあれば、呼び出し関係の薄い部分で分割できると判断できます。認知度が低いのですが、イケてるコールグラフ作成ツールが、calligraphyです。

% cabal install calligraphy

GHCが生成する IDE Infoからコールグラフを作成します。ほーら、イケてる気がするでしょう?使い方は、こんな感じです。

% cabal build --ghc-options=-fwrite-ide-info
% calligraphy Network.TLS.Handshake.Client --output-png out.png

この場合のout.pngは、こんな感じです。

オプションがたくさんあるので、いろいろ試してみてください。IDE Infoを使うので、calligraphyをビルドしたGHCのバージョンと、プロジェクトに使う GHC のバージョンは一致させる必要があります。

リアライザー

Haskelldataシリアライズするには、歴史的に binary や cerial が使われてきました。これらのライブラリでは、定義したdataに対するシリアライザーやデシリアライザーを手書きする必要があります。

最近のライブラリでは、シリアライザーやデシリアライザーを自動生成できます。その一つであるserialiseの使い方を紹介します。

% cabal install serialise

基本型は、デフォルトで扱えるようになっています。

% ghci    
ghci> import Codec.Serialise
ghci> serialise (1 :: Int)
"\SOH"

シリアライズできました。では、デシリアライズを試してみましょう。

ghci> deserialise $ serialise (1 :: Int)
*** Exception: DeserialiseFailure 0 "expected null"

作成されたバイナリには、型情報がないんですね。(出力が1バイトの時点で気づけって?) 型を補ってみましょう。

ghci> deserialise $ serialise (1 :: Int) :: Int
1

ヤッホー! では、自前の型を作ってみます。シリアライザを自動生成するには、DeriveGeneric 言語拡張が必要です。

ghci> :set -XDeriveGeneric 
ghci> import GHC.Generics
ghci> data Tree = Leaf | Node Int Tree Tree deriving (Generic)

そして、以下の魔法を唱えると、シリアライザ&デシリアライザが自動生成されます。

ghci> instance Serialise Tree
ghci> serialise Leaf
"\129\NUL"
ghci> serialise $ Node 1 Leaf Leaf
"\132\SOH\SOH\129\NUL\129\NUL"

やったね!

軽量スレッドのスタック

軽量スレッドが生成されたとき、スタックの最初の大きさは 1KiB です。スタックが消費されたら自動的に伸びますが、伸びる大きさは 32KiBです。つまり、1KiBの次は33KiBになってしいます。なので、たくさん軽量スレッドを使うと、使用するメモリの大半をスタックが占めるようになります。

実際は、スタックの大きさは2KiBでも事足りているかもしれません。伸びたスタックが縮むことはありません。スタックの伸びる大きさを制御する RTS オプションが -kcです。指定できる最小値は、2KiB(-kc2k)のようです。

この事実を知ってから、僕が作っているサーバの cabal ファイルには、以下の行を入れるようになりました。(他の RTS オプションの意味は、各自で調べてください。)

 ghc-options: -Wall -threaded -rtsopts "-with-rtsopts=-qn1 -A32m -kc2k"

Developing network related libraries in Haskell in 2022FY

This article is my annual report of 2022FY(fiscal year in Japan; from April 2022 to March 2023). My mission in IIJ is contribute to standardizations of new network protocols by their implementations. As you may know, I maintain some network-related libraries in Haskell.

HTTP/2

Background: the http2 library originally provided HTTP/2 frame and HPACK encoder/decoder only. This was integrated into Warp to provide the HTTP/2 server functionality. This functionality was extracted into the server side in http2. Then the client side was implemented.

Version 3.0.x were released in 2021FY to fix the vulnerabilities reported in "HTTP/2 implementations do not robustly handle abnormal traffic and resource exhaustion".

Version 4.0.0

RFC 9113 was published in June 2022 and obsoleted RFC 7540. The main purpose of this version is to catch up this new RFC. The major version up was due to a lot of breaking changes.

Version 4.1.0

The server side has been tested well in the real world but the client side is not. Evgeny Poberezkin kindly reported some bugs relating to streaming in the client side. This version should fix these bugs. I thank him a lot.

The major version up again because the internal data type was changed resulting in the build break of http3. A new version of http3 to catch up this change was also released.

QUIC

Background: the quic/http3 library have been developed since 2019 and were released after RFC9000 was published in 2021.

Version 0.0.x adopts the fusion crypto engine for performance reasons. After releasing version 0.0.x, I noticed that it supports the Intel architecture only. I should have quickly worked around but my interest went to QUIC version 2 and the version negotiation. After implementing these new technologies, I had integrated the fusion and cryptonite to let run the quic library on all platforms.

At this moment, I had a dilemma: I cannot release a new version of quic since the numbers of QUIC version 2 and the trasnport parameter for the version negotiation would change. (The value of QUIC version 2 was 0x709a50c4 in drafts and is 0x6b3343cf finally. It's not 0x00000002 at all!)

Meanwhile, I spend time to support Windows. The UDP implementation of Windows is really awkward. See the following blog article for more information.

Based on this experience, I have released the network-udp library which provides best current practice for UDP clients and servers. (This library is also used in the dnsext-do53 library described later.)

Though RFCs of QUIC version 2 and the version negotiation have not been released, the numbers have been fixed. So, I released the quic library v0.1.0 finally in Feb 2023.

DNS

Background: to implement anti-spam technologies such as SPF and DKIM, I have started implementing the dns library purely in Haskell since 2010. Thanks to GHC's concurrency, the stub resolver functionality is highly concurrent. Fortunately this library is used widely but unfortunately two down-sides were turned out:

  1. Resource records are not extensible: resource records are implemented as a sum type. The third party library cannot extend them. The only way to extend them is to send a pull request to the dns library.
  2. Resource records are not friendly to caching: some resource records use ByteString internally. So, if they are cached for a long time, fragmentation happens.

dnsext

It appeared impossible to maintain backward compatibilities to the dns library. So, new libraries whose prefix is dnsext- were created.

  • dnsext-types: basic types with encoders/decoders. To solve 1), I introduced typeclasses. To fix 2), ShortByteString is used as an internal representation.

The following is the definition of extensible resource records:

class (Typeable a, Eq a, Show a) => ResourceData a where
    resourceDataType :: a -> TYPE
    putResourceData  :: CanonicalFlag -> a -> SPut ()

-- | A type to uniform 'ResourceData' 'a'.
data RData = forall a . (Typeable a, Eq a, Show a, ResourceData a) => RData a

A basic type Domain is defined using ShortByteString instead of ByteString:

data Domain = Domain {
    -- The representation format. Case-sensitive, escaped.
    representation  :: ShortByteString
    -- Labels in wire format. Case-sensitive, not escaped.
  , wireLabels      :: [ShortByteString]
  -- | Eq and Ord key for Canonical DNS Name Order.
  --   Lower cases, not escaped.
  --   https://datatracker.ietf.org/doc/html/rfc4034#section-6.1
  , canonicalLabels :: ~[ShortByteString]
  }
  • dnsext-do53: DNS over UDP port 53

The following is a typical usage of the stub resolver:

> withLookupConf defaultLookupConf $ \env -> lookupA env "www.iij.ad.jp"
Right [202.232.2.180]

My current interest is SVCB(Service Binding)/HTTPS resource records and DNS over new transport protocols.

  • dnsext-svcb: SVCB related resource records. This is an extension example of dnsext-types.
  • dnsext-dox: DNS over HTTP2, HTTP3, TLS and QUIC

After Kei Hibino joined to IIJ, he has focused implementing DNSSEC verifier and a full resolver.

  • dnsext-dnssec: DNSSEC verifier
  • dnsext-full-resolver: a full resolver (aka a cache server)

He wrote three articles on this topic in Japanese (article 1, article 2, article 3).

dnsext-full-resolver provides a command line interface called dug. It shows how iterative resolve works if the -i option is specified:

% dug -i www.iij.ad.jp
resolve-just: dc=0, ("www.iij.ad.jp.",A)
    "a.root-servers.net." ["198.41.0.4","2001:503:ba3e::2:30"]
    "b.root-servers.net." ["199.9.14.201","2001:500:200::b"]
    "c.root-servers.net." ["192.33.4.12","2001:500:2::c"]
        ...
iterative: selected addrs: (198.41.0.4,"jp.",A)
iterative: selected addrs: (2001:503:ba3e::2:30,"jp.",A)
iterative: selected addrs: (199.9.14.201,"jp.",A)
...
    "a.dns.jp." ["203.119.1.1","2001:dc4::1"]
    "b.dns.jp." ["202.12.30.131","2001:dc2::1"]
    "c.dns.jp." ["156.154.100.5","2001:502:ad09::5"]
        ...
iterative: selected addrs: (203.119.1.1,"ad.jp.",A)
iterative: selected addrs: (2001:dc4::1,"ad.jp.",A)
iterative: selected addrs: (202.12.30.131,"ad.jp.",A)
...
    "a.dns.jp." ["203.119.1.1","2001:dc4::1"]
    "b.dns.jp." ["202.12.30.131","2001:dc2::1"]
    "c.dns.jp." ["156.154.100.5","2001:502:ad09::5"]
        ...
iterative: selected addrs: (203.119.1.1,"iij.ad.jp.",A)
iterative: selected addrs: (2001:dc4::1,"iij.ad.jp.",A)
iterative: selected addrs: (202.12.30.131,"iij.ad.jp.",A)
...
    "dns0.iij.ad.jp." ["210.130.0.5","2001:240::105"]
    "dns1.iij.ad.jp." ["210.130.1.5","2001:240::115"]
iterative: selected addrs: (210.130.0.5,"www.iij.ad.jp.",A)
iterative: selected addrs: (2001:240::105,"www.iij.ad.jp.",A)
iterative: selected addrs: (210.130.1.5,"www.iij.ad.jp.",A)
...
    "dns0.iij.ad.jp." ["210.130.0.5","2001:240::105"]
    "dns1.iij.ad.jp." ["210.130.1.5","2001:240::115"]
resolve-just: selected addrs: (210.130.0.5,"www.iij.ad.jp.",A)
resolve-just: selected addrs: (2001:240::105,"www.iij.ad.jp.",A)
resolve-just: selected addrs: (210.130.1.5,"www.iij.ad.jp.",A)
resolve-just: selected addrs: (2001:240::115,"www.iij.ad.jp.",A)
--------------------
;; HEADER SECTION:
;Standard query, NoError, id: 8338
;Flags: Authoritative Answer


;; OPTIONAL PSEUDO SECTION:
;UDP: 1232, Data:[]

;; QUESTION SECTION:
;www.iij.ad.jp.     IN  A

;; ANSWER SECTION:
www.iij.ad.jp.  300(5 mins) IN  A   202.232.2.180

If the -d auto option is specified, dug first resolves SVCB RR and selects DNS over X according to its response.

% dug @94.140.14.140 -d auto www.iij.ad.jp
;; 2a10:50c0::2:ff#443/HTTP/3, Tx:42bytes, Rx:58bytes, 132usec

;; HEADER SECTION:
;Standard query, NoError, id: 43730
;Flags: Recursion Desired, Recursion Available

;; OPTIONAL PSEUDO SECTION:
;UDP: 0, Data:[]

;; QUESTION SECTION:
;www.iij.ad.jp.     IN  A

;; ANSWER SECTION:
www.iij.ad.jp.  300(5 mins) IN  A   202.232.2.180

We don't have a releasing plan at this moment. We should concentrate the field test of the full resolver in 2023FY.