C# ( .NET Core)で作る自作TURNサーバー 〜開発のTipsとプロトコルの説明を添えて〜 #nttcom_ac2021

はじめに

この記事は、 NTT Communications Advent Calendar 2021 12日目の記事です。

皆様こんにちは。NTTコミュニケーションズ イノベーションセンターの@sublimerです。今年の4月にNTTコミュニケーションズに入社し、SkyWayの開発・運用の業務を行っています。
本記事では、C# ( .NET Core)でTURNサーバーの自作に挑戦している話を書いていこうと思います。まだ完成はしていないのですが、TURNサーバーのプロトコルの仕組みやサーバー開発の作業の進め方などを紹介します。

作っているTURNサーバーの紹介

C#( .NET Core)製のTURNサーバー、「doturn」です。(dotnetのTURNサーバーなので)
github.com
現在は最低限のSTUNサーバー(RFC5389)の機能が動く状態で、TURNサーバーの機能は鋭意開発中です。TURNサーバーの勉強とC#の勉強を兼ねて開発をしているため、あまりきれいなコードではありません。まずは動くものを作り、追々リファクタリングができればいいなと思っています。

TURNサーバーとは

TURNサーバーについて説明する前に、WebRTCについて簡単に説明します。
WebRTCはP2Pで映像や音声等のデータを送受信するための技術で、複数のプロトコルAPIによって構成されています。WebRTCには「Web」と付いていますが、Webブラウザだけでなく、AndroidiOSなどのモバイルアプリ、RaspberryPiなどのシングルボードコンピューターからも利用できる、マルチプラットフォームな技術です。SkyWayでは、これらのプラットフォーム向けのSDKやソフトウェアも提供しています。

ここからはTURNサーバーの説明です。
前述したとおり、WebRTCの通信はP2Pが基本です。しかしながら、世の中にはP2Pで直接通信できない環境もあります。P2Pで通信したい2者の間にファイアウォールやNATがある環境では、そうではない環境と比べて直接通信できない可能性が高くなります。そのような環境でもWebRTCの接続ができるように、通信を中継してくれるサーバーがTURNサーバーです。
TURNサーバーの仕事は、片方から来たパケットをそのまま通信したい相手に受け流すだけです。TURNサーバーはWebRTCの暗号化されたパケットを暗号化されたままの状態で受け流し、復号の処理は行いません。ですので、TURNサーバーを介した通信はP2Pで直接通信した場合と同じくセキュアなものになります。余談ですが、STUNやTURNはWebRTCのために作られたものではなく、WebRTC登場以前から存在しているプロトコルです。WebRTC以外では、P2Pで通信するゲームなどでNAT超えをする際に使われたりしています。

ところで、「TURNサーバーを自作する」という話にも関わらず、先にSTUNサーバーを実装したことが気になった方がいるかも知れません。実はTURNのプロトコルは、RFCにも書かれているように、STUNの拡張として作られています。*1

TURN is an extension to the STUN (Session Traversal Utilities for NAT) protocol [RFC5389].

STUNはTURNほど実装が難しくなかったので、まずSTUNサーバーを実装してからTURNサーバーを実装することにしました。

TURNサーバーの作り方

基本的には、パケットキャプチャーの結果とRFCを行ったり来たりしながら開発しています。既存のTURNサーバーとの通信をパケットキャプチャーし、その結果を見ながら同じようなパケットを送受信できるサーバーを作っていきます。以下の画像は、SkyWayのTURNサーバーと通信した際のパケットをWiresharkでキャプチャーしたものです。(認証情報や一部のIPアドレスをマスクしています)
f:id:sublimer:20211205155600p:plain
キャプチャーしたパケットをひとつひとつ見ながら、サーバーが同じパケットを返すように処理を実装しています。また、パケットキャプチャーだけでは分からなかった挙動に関しては、RFCを読んで処理を実装しています。

自作したTURNサーバーの動作確認は、実際にブラウザでTURNを経由するWebRTCの通信を繋いで行っています。ブラウザはTURNのクライアントとして正しく動作するため、自分が実装しているTURNサーバーの挙動が正しいかどうかを確認するためのクライアントとして最適です。また、簡単に挙動を確認する際はncコマンドを使って動作確認することもあります。「WebRTC For The Curious」というサイトでは、ncコマンドを使ってSTUNサーバーのレスポンスを確認する方法が紹介されています。*2

$ stunserver=stun1.l.google.com;stunport=19302;listenport=20000;echo -ne "\x00\x01\x00\x00\x21\x12\xA4\x42TESTTESTTEST" | nc -u -p $listenport $stunserver $stunport -w 1 | hexdump -C
00000000 01 01 00 0c 21 12 a4 42 54 45 53 54 54 45 53 54 |....!..BTESTTEST|
00000010 54 45 53 54 00 20 00 08 00 01 6f 32 7f 36 de 89 |TEST. ....o2.6..|
00000020

TURNのパケットの構造

RFCに書かれているTURNのパケットの構造を説明します。
前述したとおり、TURNはSTUNの拡張なので、パケットの構造としてはSTUNと共通の部分もあります。ですので、ここではSTUNのパケットの構造を説明します。まず、STUNのパケット(STUNメッセージ)はヘッダと0個以上の属性から構成されます。

STUNヘッダー

STUNヘッダーは20バイト(160bit)で、以下のデータから構成されています。

サイズ
STUNメッセージタイプ 14bit クラス(2bit)とメソッド(12bit)の組み合わせ
メッセージ長 16bit STUNメッセージの長さ(ヘッダーのサイズは除く)
マジッククッキー 32bit 0x2112A442
トランザクションID 96bit クライアント側が生成した値

また、STUNメッセージの最上位2bitは0と定められている(MUST)ため、2+14+16+32+96=160bitとなります。ここで、STUNメッセージタイプは、以下のクラスとメソッドの組で表されます。*3

クラス C1 C0
request 0 0
indication 0 1
success response 1 0
error response 1 1
メソッド M11~M0 RFC
binding 0b000000000001 RFC5389
allocate 0b000000000011 RFC5766
refresh 0b000000000100 RFC5766
send 0b000000000110 RFC5766
data 0b000000000111 RFC5766
create permission 0b000000001000 RFC5766
channel bind 0b000000001001 RFC5766

これらのC0〜1、M0〜11を以下のパターンに当てはめて、14bitのSTUNメッセージタイプが構成されます。

M11 M10 M9 M8 M7 C1 M6 M5 M4 C0 M3 M2 M1 M0

例えば、bindingのsuccess responseは0b00000100000001で、先頭2bitの00を加えて0x0101、allocateのerror responseは0b00000100010011で、先頭2bitの00を加えて0x0113のように表現されます。

STUN属性

STUN属性は可変長で、以下のデータから構成されています。*4

サイズ
STUN属性タイプ 16bit RFCで定められいている値
長さ 16bit STUN属性の長さ(STUN属性タイプと長さは除く)
可変長(4の倍数) STUN属性の値

STUN属性タイプのうち、これまでに実装したのは以下のタイプです。

STUN属性タイプ(一部)
mapped address 0x0001
username 0x0006
message integrity 0x0008
error code 0x0009
lifetime 0x000D
xor peer address 0x0012
realm 0x0014
nonce 0x0015
xor relayed address 0x0016
requested transport 0x0019
xor mapped address 0x0020
software 0x8022
alternate server 0x8023
fingerprint 0x8028

これらの属性は基本的にはどのような順番で並べてもよいのですが、fingerprint属性だけは末尾の属性でなければなりません。(MUST)*5fingerprint属性は、fingerprint属性自体を除いたSTUNメッセージについて、巡回冗長検査の一種であるCRC-32を計算し、0x5354554eとのXORを取った値となります。また、message integrity属性の後に続くfingerprint属性を除く属性は無視される(MUST)ため、末尾から2番目の属性はmessage integrity属性である場合がほとんどとなります。*6

With the exception of the FINGERPRINT attribute, which appears after MESSAGE-INTEGRITY, agents MUST ignore all other attributes that follow MESSAGE-INTEGRITY.

message integrity属性は、ユーザー名、レルム、パスワードをコロンで結合した文字列のMD5をキーとしたHMAC-SHA1の値*7で、message integrity属性の直前の属性までのSTUNメッセージを対象として計算します。このときのSTUNヘッダーのメッセージ長に指定する値は、message integrity属性までのSTUNメッセージの長さとなります。ですので、message integrity属性の後にfingerprint属性がある場合は、fingerprint属性の長さを引いた値をSTUNヘッダーに入れてmessage integrity属性を計算し、その後でfingerprint属性の値も含めたSTUNメッセージの長さをSTUNヘッダーに入れてあげる必要があります。この部分は実装する際にハマったポイントで、RFCをしっかり読んでようやく理解できました。
文章での説明はわかりにくいかと思うので、Allocate Success Responseを例に図で説明します。
今回実装しているTURNサーバーのAllocate Success Responseのパケットは、以下の図のような構造になっています。
f:id:sublimer:20211205234854p:plain
message integrity属性を計算する際は、STUNヘッダーのMessage Lengthの値を、fingerprint属性の長さを引いた値である64に設定してからHMAC-SHA1の値を計算します。HMAC-SHA1の計算対象は、STUNヘッダーの先頭からmessage integrity属性の直前の属性であるsoftware属性までとなります。
f:id:sublimer:20211207212604p:plain
その後、STUNヘッダーのMessage Lengthの値を本来の値である72にして、CRC-32の値を計算します。
CRC-32の計算対象はfingerprint属性自体を除いたSTUNメッセージなので、message integrity属性までとなります。
f:id:sublimer:20211205235330p:plain

TURNサーバーでは、このようにしてSTUNメッセージの属性を計算し、レスポンスのパケットを組み立てています。

おわりに

TURNサーバーをC#で作っている話や、TURNのプロトコルの仕組みを説明しました。TURNサーバーの開発を通して、これまで以上にパケットの気持ちがわかるようになった気がします。バイナリデータを一つ一つ読み解き、仕様通りの挙動を実装できたときは非常に楽しいです。
ですが、まだTURNサーバーは未完成です。これからも引き続き、TURNサーバーの実装やプロトコルの勉強を頑張っていきたいと思います。

以上、NTT Communications Advent Calendar 2021 12日目、「C# ( .NET Core )で作る自作TURNサーバー 〜開発のTipsとプロトコルの説明を添えて〜」でした!
それでは、明日もお楽しみに!!