[Go言語で] QUICの通信を実装してみて通信内容を見てみたい！

:::note info
とてもとても（4ヶ月）遅刻してしまいましたが、この記事は[DeNA 25新卒 Advent Calendar 2024](https://qiita.com/advent-calendar/2024/dena-25-newgrad)の記事です。他の方々の記事もぜひご覧ください！
:::

<!-- TODO: 記事のリンク -->
前回、前々回の記事と、C言語およびGo言語でTLSサーバを実装することに挑戦しました。

ここで、近年、あらたなWeb技術としてQUICというものが登場し普及しだしているのをご存知でしょうか。従来のTCPおよびHTTP/1.1, HTTP/2の問題点を解決すべく、**新たなトランスポート層のプロトコル**として策定されたプロトコルです。

「新たな」と言いましたがその実装は内部的にはUDPのパケットを利用しており、UDPにQUICのパケットを乗せて送受信するという仕組みになっています。それにより、マシン側（OSなど）の対応は最低限でQUICの通信を利用できるというメリットもあります。

また、このQUICを下層に使ったアプリケーション層のプロトコルとして**HTTP/3**というものも標準化されました。これらのプロトコルでは、特に低品質な通信環境において従来の通信プロトコルよりも高い通信性能をもたらすと言われています。

QUICの詳細な説明はより詳しい別の記事に任せるとして、QUICが自分の卒業研究のテーマだったこともあり、この記事では**Go言語でQUICのクライアント・サーバアプリケーションを実装し、その通信を見てみる**ということに挑戦してみたいと思います。

ちなみに、**QUICのプロトコルでは内部的にTLSを利用するという仕様**になっており、QUICで通信することはすなわちTLSで暗号化された通信をすることに該当します。そのため今回の実装は、前回の記事の実装と非常に似ており、前回のものにQUIC特有の記述を追加したといってもいい程です。以下の実装の説明では、その差分を中心にまとめます。

## 先にまとめ

<details>
<summary>サーバのコード</summary>

```go:server/main.go
package main

import (
	"bufio"
	"context"
	"crypto/tls"
	"log"

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

func main() {
	cer, err := tls.LoadX509KeyPair("server.crt", "server.key")
	if err != nil {
		log.Fatal("loadkeys: ", err)
	}

	tlsConf := &tls.Config{Certificates: []tls.Certificate{cer}}
	quicConf := &quic.Config{}

	ln, err := quic.ListenAddr("127.0.0.1:12345", tlsConf, quicConf)
	if err != nil {
		log.Fatal("listen addr: ", err)
	}
	defer ln.Close()

	log.Print("Start Server:")

	for {
		conn, err := ln.Accept(context.Background())
		if err != nil {
			log.Fatal("accept: ", err)
		}

		stream, err := conn.AcceptStream(context.Background())
		if err != nil {
			log.Fatal("accept stream: ", err)
		}

		log.Print("New Client Connection Accepted")
		go func(stream quic.Stream) {
			s := bufio.NewScanner(stream)
			for s.Scan() {
				msg := s.Text()
				log.Printf("Accept Message: `%s`", msg)
				_, err := stream.Write([]byte(msg))
				if err != nil {
					log.Print("write: ", err)
				}
			}
		}(stream)
	}
}
```
</details>

<details>
<summary>クライアントのコード</summary>

```go:client/main.go
package main

import (
	"context"
	"crypto/tls"
	"log"
	"os"

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

func main() {
	w, err := os.Create("keylog.log")
	if err != nil {
		log.Fatal("open file: ", err)
	}

	tlsConf := &tls.Config{
		InsecureSkipVerify: true,
		KeyLogWriter:       w,
	}
	quicConf := &quic.Config{}

	conn, err := quic.DialAddr(context.Background(), "127.0.0.1:12345", tlsConf, quicConf)
	if err != nil {
		log.Fatal("dial: ", err)
	}

	stream, err := conn.OpenStreamSync(context.Background())
	if err != nil {
		log.Fatal("open stream: ", err)
	}
	defer stream.Close()

	if _, err = stream.Write([]byte("Hello, golang QUIC!!\n")); err != nil {
		log.Fatal("write: ", err)
	}

	buf := make([]byte, 100)
	n, err := stream.Read(buf)
	if err != nil {
		log.Fatal("read: ", err)
	}

	log.Printf("Accept Message: `%s`", buf[:n])
}
```
</details>

ソースコードのリポジトリ

https://github.com/mochi-yu/qiita-material/tree/main/golang-quic-communication

## 実装と動作確認
### 準備
準備が必要なものは前回とほとんど同じで、「Go言語の実行環境」とパケットをキャプチャするための「`tshark`」、サーバ証明書と秘密鍵です。フォルダ構成なども前回と同様なためここでは省略します。

加えて、今回のQUICの実装には`github.com/quic-go/quic-go`というパッケージを利用しました。準標準パッケージの`golang.org/x/net/quic`というものもあったのですが、自分が使い慣れていたこともあり`quic-go`の方を採用しました。

https://pkg.go.dev/github.com/quic-go/quic-go

https://pkg.go.dev/golang.org/x/net/quic

以下のコマンドで、このパッケージも追加しておきます。

```shell
go get github.com/quic-go/quic-go
```

### サーバの実装

まずはサーバの実装です。

<details>
<summary>サーバのコード</summary>

```go:server/main.go
package main

import (
	"bufio"
	"context"
	"crypto/tls"
	"log"

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

func main() {
	cer, err := tls.LoadX509KeyPair("server.crt", "server.key")
	if err != nil {
		log.Fatal("loadkeys: ", err)
	}

	tlsConf := &tls.Config{Certificates: []tls.Certificate{cer}}
	quicConf := &quic.Config{}

	ln, err := quic.ListenAddr("127.0.0.1:12345", tlsConf, quicConf)
	if err != nil {
		log.Fatal("listen addr: ", err)
	}
	defer ln.Close()

	log.Print("Start Server:")

	for {
		conn, err := ln.Accept(context.Background())
		if err != nil {
			log.Fatal("accept: ", err)
		}

		stream, err := conn.AcceptStream(context.Background())
		if err != nil {
			log.Fatal("accept stream: ", err)
		}

		log.Print("New Client Connection Accepted")
		go func(stream quic.Stream) {
			s := bufio.NewScanner(stream)
			for s.Scan() {
				msg := s.Text()
				log.Printf("Accept Message: `%s`", msg)
				_, err := stream.Write([]byte(msg))
				if err != nil {
					log.Print("write: ", err)
				}
			}
		}(stream)
	}
}
```
</details>

Listnerを用意しforループで接続を待ち続けるのはTLSのときと同じです。ただし、Listnerは`quic.ListenAddr()`で生成した`*quic.Listener`型のものです。

接続を受け付けたら、TLSではコネクションを取得してそれを用いてやり取りしていましたが、QUICの処理ではコネクションを受け付けた後に **ストリーム（`quic.Stream`）** を用意して通信を行いました。

QUICとTLSのサーバ側の処理の大きな差分は、このループ内の処理で`net.Conn`を使うか`quic.Stream`を使うかという部分でした。

```go:server/main.go（抜粋）
	for {
		conn, err := ln.Accept(context.Background())
		if err != nil {
			log.Fatal("accept: ", err)
		}

		stream, err := conn.AcceptStream(context.Background())
		if err != nil {
			log.Fatal("accept stream: ", err)
		}

		log.Print("New Client Connection Accepted")
		go func(stream quic.Stream) {
      // TLSの際とほぼ同じ処理のため省略
		}(stream)
	}
```

### クライアントの実装

続いてクライアントの実装です。

<details>
<summary>クライアントのコード</summary>

```go:client/main.go
package main

import (
	"context"
	"crypto/tls"
	"log"
	"os"

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

func main() {
	w, err := os.Create("keylog.log")
	if err != nil {
		log.Fatal("open file: ", err)
	}

	tlsConf := &tls.Config{
		InsecureSkipVerify: true,
		KeyLogWriter:       w,
	}
	quicConf := &quic.Config{}

	conn, err := quic.DialAddr(context.Background(), "127.0.0.1:12345", tlsConf, quicConf)
	if err != nil {
		log.Fatal("dial: ", err)
	}

	stream, err := conn.OpenStreamSync(context.Background())
	if err != nil {
		log.Fatal("open stream: ", err)
	}
	defer stream.Close()

	if _, err = stream.Write([]byte("Hello, golang QUIC!!\n")); err != nil {
		log.Fatal("write: ", err)
	}

	buf := make([]byte, 100)
	n, err := stream.Read(buf)
	if err != nil {
		log.Fatal("read: ", err)
	}

	log.Printf("Accept Message: `%s`", buf[:n])
}
```
</details>

こちらも、サーバ側の処理と同様にダイアルしてサーバとのコネクションを生成するまではほとんど同じですが、コネクションを確立した後で`conn.OpenStreamSync()`によって**ストリーム**を生成しそれを介してデータのやり取りを行いました。

ここで生成したストリームのインスタンスは、`io.Writer`と`io.Reader`のインタフェースを実装しているようで、`Write()`や`Read()`などのメソッドを使うことができました。

```go:client/main.go（抜粋）
	stream, err := conn.OpenStreamSync(context.Background())
	if err != nil {
		log.Fatal("open stream: ", err)
	}
	defer stream.Close()

	if _, err = stream.Write([]byte("Hello, golang QUIC!!\n")); err != nil {
		log.Fatal("write: ", err)
	}
```

### 実行
実行は、ターミナルで`go run ./server/main.go`としてサーバを起動し、別のターミナルで`go run ./client/main.go`として実行します。

少しエラーも出ていますが、これで以下のように通信が行えました。

サーバ側のログ

![サーバ側のログ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351104/1706a12b-1c0e-44c7-8cae-ce70f260a4f7.png)

クライアント側のログ

![クライアント側のログ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351104/8a948564-1045-4ac9-b7ea-58789d272b4e.png)

## パケットの確認と暗号化通信の復号

まずは暗号化されている状態のパケットを見てみます。TLSの実装の時には各パケットのプロトコルが`TCP`や`TLS`と表示されていました。今回のキャプチャではプロトコルがしっかりと`QUIC`と表示されています。

そして、各パケットのペイロードは案の定暗号化されており、読むことはできませんでした。

![QUIC通信も暗号化されている](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351104/2ee1ea33-fca3-4071-9cbd-0d5a793758fd.png)

この暗号化を解く方法は、**QUICの通信でもTLSの時と同じ手順**で可能でした。上部タブから「編集」→「設定」→「Protocols」→「**TLS**」と選んでいき、「(Pre)-Master-Secret log filename」に`keylog.log`を設定します。

QUICの設定ではなく**TLS**の設定から変更するのでお気をつけください。

こうすることで、今回も送受信した文字列を読むことができました！

![暗号化を復号](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351104/6c509ed1-b961-4e1b-8c9a-9680217ed9c8.png)

## おわりに

この記事ではQUICによるデータ通信をGo言語で実装し、そのパケットを見てみました。QUICは大学の卒業研究で取り組んだテーマだったので、研究の過程で仕様については色々と学習してきました。そこで得ていた知識が、これまた実際に動いているデータとして見ることができ、また生きた学びを得られたと感じています。

QUICの通信も、色々とカスタマイズできたはずなので、それをしていってパケットがどのように変化するのか見てみるのも面白いかもしれません。

それから、QUICは現在も発展と普及の最中にある技術であると思っています。卒業研究でのゆかりもあり、QUICという技術が今後どう展開していくのかとても楽しみです。このあたり、この先も広くアンテナを広げて情報を得続けていけたらなと思います！