quic-go が QUIC DATAGRAM に対応したので早速試してみる - aptpod Tech Blog

aptpod Tech Blog

株式会社アプトポッドのテクノロジーブログです

quic-go が QUIC DATAGRAM に対応したので早速試してみる

f:id:aptpod_tech-writer:20210127104136j:plain

はじめに

VPoP として弊社の製品全体を統括しております、岩田です。

弊社では以前から、自社製品が使用する通信方式の下回りとして QUIC を使用することができないか 、継続的に調査や検討を行ってきました。QUIC が HTTP/3 をメインターゲットとして最低限の仕様策定を進める方向になって以降、QUIC 検討に対する社内の熱量も多少減退してはいたものの、昨年の WebTransport 周辺の動きを受けて、再度勢いを取り戻しつつあります。

QUIC DATAGRAM は、QUIC を HTTP 向けの ベターTCP としてだけではなく、UDPベース であることを生かしたユースケースで利用できるようにするための追加仕様で、UDP Like な通信を導入することで QUIC の用途を映像伝送やゲームなどのリアルタイム通信に拡張しようとするもの です。QUIC DATAGRAM 自体は、提唱されてから意外と時間の経っている仕様ではありますが、ここ最近 WebTransport や WebRTC での活用という話題が出始めて以降、動きが活発化してきているように感じています。

そんな矢先、以前から検証に使用していた Go製の QUIC ライブラリである quic-go が QUIC DATAGRAM に対応した ので、早速試して記事にしてみたいと思います!

github.com

本記事の内容

QUIC DATAGRAM について

※ ここから説明が長く続きます。単純に quic-go や「試してみた」に興味があるだけの方は、Go製 QUIC ライブラリ quic-go までスッと読み飛ばしてしまって構いません。

そもそも QUIC とは

現在では、QUICに関する情報は世の中に溢れてきており、わざわざ解説するまでもなく皆さんご存知かと思いますので、ざっくりとだけ説明をしておきます。

QUICは、GoogleがWebをより高速化するために考案した、UDPベースの新しいトランスポートプロトコルです。当初はHTTP/2のいくつかの課題を解決するために提案されたため、UDPベースといっても データグラムでの伝送を行うものではなく、UDPの上にTCP Like な信頼性のあるストリーム伝送を再定義したもの 、という位置づけになります。現在では、HTTP over QUIC は HTTP/3 という名称で呼ばれるようになっていますので、こちらの名前の方が馴染みがある、という方も多いかも知れません。

下の図は、ちょっと古いですがよく引用される有名な図です。これを見ると、トランスポート〜アプリケーションのレイヤでのQUICの位置づけがよく分かると思います。

f:id:aptpod_tech-writer:20210125121546p:plain
QUIC のプロトコルスタック

(引用: https://datatracker.ietf.org/meeting/98/materials/slides-98-edu-sessf-quic-tutorial/ )

Google がQUICを発表した のが2013年、IETFに提唱したのが2015年です。その後、2016年に IETFワーキンググループ が立ち上がり、今に至るまで標準化活動が続けられています。昨年内におおよその仕様が FIX しており、今年いつRFC化されてもおかしくない、というのが現在のステータスです。

QUIC DATAGRAM とは

QUIC DATAGRAM は、現在検討が進んでいる QUIC の拡張仕様で、QUIC のコネクションの中で UDP Like なデータグラム伝送を実現する仕組み です。正式なRFCドラフトの名称は「An Unreliable Datagram Extension to QUIC」で、draft-pauly-quic-datagram-05 が現在の最新のようです。

現状、映像/音声などのメディアデータ伝送やゲームなどのリアルタイム性が求められる通信では、業界や要件、過去の慣例等によって様々なプロトコルが使用されている状況 ですが、その多くは、セキュリティに不安があったり、必要以上に複雑な階層構造を持っていたり、どれも100点満点とはいえないものばかりです。

そのような状況の中、QUIC が UDP をベースとしており、セキュリティ機能も保有していたので、せっかくならばデータグラム通信もさせてしまいましょう、というのが QUIC DATAGRAM 策定のモチベーションとなっています。

なぜ QUIC / QUIC DATAGRAM に着目するのか

弊社の現在の製品群の主なミッションは、大量かつ高頻度なデータストリームを、エンドツーエンドで、できる限りリアルタイムに伝送すること です。例えば、自動車の遠隔制御などのユースケースでは、人が確認するための大量の映像データには「リアルタイム性」、制御に使用するコマンドデータは「確実性」という相反する要件が求められます。

現行版の製品は、WebSocket をベースのプロトコルとして使用していますが、動画などのメディアデータの伝送に対する要望が高まり続けた結果、現在ではデータの詰まり遅延が顕著な問題となるようになってきました。これは、WebSocket が持つ TCP Like な伝送方式のリアルタイム性能の限界 を示しており、早急に UDP Like なデータグラム伝送への切り替えが必要です。一方で、遠隔モニタリングや遠隔制御といったユースケースを満たさなければいけない以上、コマンドデータのような 確実性を求める伝送もなくなるわけではありません

QUIC や、QUIC の特徴を最大限に生かした WebTransport といった新しいプロトコルは、TCP Like / UDP Like な2種類の伝送方式によって、我々を悩ませている 相反する2つの要件を同一のコネクションに収容する ことができ、これを採用することで通信アーキテクチャの大幅なシンプル化を見込むことができます。他にも、モビリティとコネクションマイグレーションの相性の良さなど、総じてQUICは 我々のユースケースにはピッタリのプロトコルであると考えており、提案当初から継続して技術動向を追いかけています。

Go製 QUIC ライブラリ quic-go

※ ここからやっと技術的な内容が始まります。お待たせしました。 技術的な内容はちょっと難しすぎる...という方は おわりに だけでも読んでいってやってください。

quic-go は、100% ピュアな Go で実装された QUIC のライブラリです。弊社はサーバーサイドを主に Go で実装していますので、QUIC を使用したプロトタイピングにはこちらのライブラリをよく利用しています。

ちなみに、Google の公式ライブラリというわけではないですが、どうやら中の人は Google のソフトウェアエンジニアのようで、IETF での標準化活動が本格化する前から現在に至るまで、かなり活発に開発が続けられています。

v0.18.0 までは、サポートしているドラフトバージョンを明示していなかったこともあり ( “It roughly implements the IETF QUIC draft, although we don't fully support any of the draft versions at the moment.“ ) 、他のライブラリとの互換性の面で少し使いにくいところもあったのですが、現在は draft-29 と draft-32 のサポートを明示するなど、段々と改善してきているようです。また、他のライブラリから利用される例 も出てきており、Golang で QUIC を利用する上でのデファクトスタンダードとなりつつあるライブラリです。

quic-go の QUIC DATAGRAM のサポート状況としては、以前より datagram ブランチというひとつのブランチでほそぼそと開発が続けられていたのですが、昨年末に突如として開発が再開し、あれよあれよという間に master ブランチまでマージされました。こういった経緯もあり、quic-go における QUIC DATAGRAM は、まだタグ付けされたバージョンには入っておらず、master にのみ存在するできたてホヤホヤの機能となります。ちなみに、quic-go の GoDoc は最新のタグである v0.19.3 をベースにしているため、まだドキュメントにすら現れていません。

今回のブログネタは、待ち望んでいた QUIC DATAGRAM がついに quic-go でも使用可能になった ので、(まだタグ付けすらされていない機能ですが、少し食い気味に)とりあえず体験してみよう!というのが主な目的です。

早速試してみる

だいぶ前置きが長くなりました。ここから、実際に QUIC DATAGRAM を体験してみたいと思います。

検証用に適当なエコーサーバーを書いてみるというのも選択肢としてはあるのですが、なんと QUIC DATAGRAM には、トランスポートプロトコルを検証するためだけのアプリケーションプロトコル がRFCドラフトとして存在していますので、quic-go を使ってこちらのプロトコルを実装してみようと思います。

その検証用プロトコルは、 SiDUCK (Simple Datagram Usability and Connectivity Kata) と呼ばれるもので、draft-pardue-quic-siduck-00 で規定されています。

ただ、プロトコルが定義されているといってもそんな大層なものではなく、ただ単に「クワッ(quack)」と鳴いたら「クワックワッ(quack-ack)」と返せ、それ以外が来たら特定のエラーコードで返せ、というだけのめちゃくちゃシンプルなものです。

”QU”IC と掛けたのか、メッセージ内容がアヒルの鳴き声 “qu“ack だったり、プロトコルの名前が Si”DUCK” だったり、頭のいい人たちはこんな言葉遊びができるんですね。おしゃれです)

その他の決まりごととしては、特定 のALPN (“siduck”) を使いなさい、というくらいしかありません。RFCは数分で読めます。

SiDUCK サーバー

まずはサーバー用に、quic-go で SiDUCK を実装したのがこちらになります。本当に簡単です。

package main

import (
    "bytes"
    "context"
    "crypto/rand"
    "crypto/rsa"
    "crypto/tls"
    "crypto/x509"
    "encoding/pem"
    "fmt"
    "log"
    "math/big"

    "github.com/lucas-clemente/quic-go"
)

func main() {
    tlsConfig := &tls.Config{
        Certificates: []tls.Certificate{tlsCert()},
        NextProtos:   []string{"siduck"}, // ALPN は "siduck" とする
    }
    quicConfig := &quic.Config{
        EnableDatagrams: true, // QUIC DATAGRAM を利用する
    }
    lis, err := quic.ListenAddr("127.0.0.1:55555", tlsConfig, quicConfig)
    if err != nil {
        panic(err)
    }

    for {
        sess, err := lis.Accept(context.TODO())
        if err != nil {
            panic(err)
        }

        go func() {
            for {
                msg, err := sess.ReceiveMessage()
                if err != nil {
                    log.Print(err)
                    return
                }

                // quack でなければエラー (0x101=DISUCK_ONLY_QUACKS_ECHO) を返す
                if !bytes.Equal(msg, []byte("quack")) {
                    sess.CloseWithError(0x101, "SiDUCK only quacks echo")
                    return
                }

                // quack だったら quack-ack を返す
                if err := sess.SendMessage([]byte("quack-ack")); err != nil {
                    log.Print(err)
                    return
                }
            }
        }()
    }
}

// オレオレ証明書を作る
func tlsCert() tls.Certificate {
    key, _ := rsa.GenerateKey(rand.Reader, 1024)
    template := x509.Certificate{SerialNumber: big.NewInt(1)}
    certDER, _ := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
    keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
    certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
    tlsCert, _ := tls.X509KeyPair(certPEM, keyPEM)
    return tlsCert
}

SiDUCK クライアント

SiDUCK に則るメリットのひとつとして、標準化された(される)検証方式であるため、他のライブラリも SiDUCK クライアントを持っている、ということが挙げられます。今回は、クライアント側はアリモノを使って楽をしてみます。

クライアントとして使用するのは、Python 製の QUIC ライブラリである aioquic です。こちらも、 quic-go と並んでかなり活発に開発が進められているライブラリでのようです。asyncio ベースなのがすこし面倒ではありますが、Python を使ってサクッと QUIC が試せるのは便利です。

github.com

このライブラリの example として、siduck_client.py というその名の通り SiDUCK のクライアントがあるので、今回はこちらを利用させていただくことにしました。

いざ疎通

先程つくったサーバーを、立ち上げて…

$ go run main.go

aioquic の SiDUCK クライアントを実行すると…

$ python3 siduck_client.py -k 127.0.0.1 55555
2021-01-25 14:00:30,093 ERROR quic [a65ec898880237e4] Could not find a common protocol version

!!

なんと疎通ができません。なんでや。

原因究明

結論からいえば、quic-go のバージョンの運用方針が原因で落とし穴にハマった ようです。今回、QUIC DATAGRAM が master にマージされたからと喜び勇んで master ブランチを使って試していたのですが、よくよく調べてみると、quic-go はタグ付けされたリリース以外はサポートバージョンをあえて絞って運用している らしく、master ブランチのサポートバージョンからは draft-29 も draft-32 も消されておりました。

https://github.com/lucas-clemente/quic-go/blob/master/internal/protocol/version.go#L30

参考: 最新のタグである v0.19.3 には、たしかに draft-29 と draft-32 があるのです…

https://github.com/lucas-clemente/quic-go/blob/v0.19.3/internal/protocol/version.go#L30

quic-go の README をきちんと読めば、タグを使え、と書いてあることには気づけたはずでしたが、今回すこし舞い上がってしまっていたようです。やはりエンジニアたるものドキュメントはきちんと読まなければいけません。反省。

When using quic-go as a library, please always use a tagged release. Only these releases use the official draft version numbers.

しかしここまで楽しみに待っていたのですから、こんなことで引き下がるわけにはいきません。(こちらにもブログ執筆のための調査時間というサンクコストがかかっているんです)

と、いうわけで、今回は 黒魔術 を使って無理やり進めていきたいと思います。

黒魔術

あえて絞られているだけなら無理やり書き換えてしまえ、ということで、絞られている値を書き換えます。(あくまで QUIC DATAGRAM を試してみたい、という欲望のままに実験を行っていますので、あまり真似はおすすめしません)

まずは、GitHub から quic-go のリポジトリを clone し、さらに []VersionNumber{VersionTLS} に絞られている値を []VersionNumber{VersionDraft29} に書き換えます。(aioquic の対応バージョンが draft-29 のようでしたので、draft-29 に書き換えることにしました。quic-go の master 実装の詳細まで詳細に追ったわけではありませんので、master ブランチが draft-29 をサポートしている保証はまったくもってありません。master ブランチの README には draft-29 とdraft-32 をサポートしていると書いてあるので、動いたらラッキーということで進めてしまいます)

さらに、今回使用するサーバー(main.go)が参照するリポジトリを、GitHubのリポジトリではなく、cloneして書き換えたローカルのリポジトリに無理やり置き換えます。

$ go mod edit -replace github.com/lucas-clemente/quic-go=/path/to/local/github.com/lucas-clemente/quic-go

さてここまでで、やっと準備が整いました。

いざ疎通(2回目)

サーバーを立ち上げて、SiDUCK クライアントを再度実行すると…

$ python3 siduck_client.py -k 127.0.0.1 55555 -q /tekitou/na/basho/qlog
2021-01-25 15:16:35,900 INFO quic [1b6e3d98e57135bd] Retrying with token (86 bytes)
2021-01-25 15:16:35,913 INFO quic [1b6e3d98e57135bd] ALPN negotiated protocol siduck
2021-01-25 15:16:35,916 INFO client sending quack
2021-01-25 15:16:35,918 INFO client received quack-ack

やりました!!アヒルがちゃんと鳴いてくれました!!!

(しかしなんとも表示が素っ気ないです)

qviz で通信を見てみる

これだけでは表示があっさりしすぎていて味気がまったくないので、qviz というツールを使ってフレームの流れを可視化してお茶を濁しておこうと思います。先程のコマンドにしれっと追加していた -q /tekitou/na/basho/qlog は、qviz で使用するファイルを出力するためのコマンドなのでした。

qviz については、すでに詳しく書かれている情報がある のでそちらに譲りますが、ざっくり概要としては、下の図のような形で、QUIC のフレームの流れを可視化することができるツールです。下の図は、siduck_client.py を使用して通信をした際に出力したファイルを、qviz を使って可視化してみたものです。

f:id:aptpod_tech-writer:20210125121210p:plain
qviz による QUIC フレームの可視化

他の通信も混ざっていて分かりにくくはありますが、aioquic 側(クライアント、左側)より Datagram が送信され、その後 quic-go 側(サーバー、右側)より Datagram が返却されている様子が、分かると思います。(上の方と下の方にある、datagram と書かれた赤い箱がそれです)

今回は一度しか SiDUCK クライアントを実行していないので、上の図に表示されている2つの Datagram はきっと “quack“ と “quack-ack“ のはずですが、念の為中身を覗いてみます。フレームをクリックするとフレームの中身が見える ようですので、2つのフレームをそれぞれ表示してみます。

f:id:aptpod_tech-writer:20210125121318p:plainf:id:aptpod_tech-writer:20210125121323p:plain
Datagram フレームの中身(左: "quack"、右: "quack-ack")

残念ながらフレームの中身まで復号化して表示してくれるわけではないようで、詳細を表示してもデータ長までしか確認することはできませんでした。とはいえ、1フレーム目が5文字(”quack”)、2フレーム目が9文字(”quack-ack”)ですので、これで自信を持って SiDUCK に沿った通信ができた、と言えそうです。

余談

本当は、Wireshark を使用して、フレームの中身を複合して、きちんとアヒルが会話している様子をお見せしたかったのですが、どうやら 現行の Wireshark は draft-29 のフレームを正しく復号できないバグを抱えている ようで、泣く泣く断念しました。

おわりに

昨年 QUIC の仕様策定に概ね目処が立ち、今年が QUIC 元年になることはほぼ確実となりました。はじめは HTTP/3 がメインターゲットとされてはいますが、WebTransport やその周辺の動きをみても、HTTP/3 をターゲットとした QUIC 本体の次には DATAGRAM の標準化がくるのではないか(きてほしい)と思っているところであります。QUIC ならびに QUIC DATAGRAM は弊社にとってはキーとなりうる技術ですので、技術進化のスピードに振り落とされないよう、最新動向にキャッチアップしていく所存です。

アプトポッドでは、お客様としての案件のご依頼はもちろんですが、求人への応募やビジネスパートナーとしての提携など、ともにビジネスを推進していく仲間を随時募集しております。弊社の技術に興味をお持ちくださった皆様、ぜひ一度お声がけいただけますと幸いです。

本年もなにとぞよろしくお願いいたします。