はじめに
この記事は、 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ブラウザだけでなく、AndroidやiOSなどのモバイルアプリ、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アドレスをマスクしています)
キャプチャーしたパケットをひとつひとつ見ながら、サーバーが同じパケットを返すように処理を実装しています。また、パケットキャプチャーだけでは分からなかった挙動に関しては、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のパケットは、以下の図のような構造になっています。
message integrity属性を計算する際は、STUNヘッダーのMessage Lengthの値を、fingerprint属性の長さを引いた値である64に設定してからHMAC-SHA1の値を計算します。HMAC-SHA1の計算対象は、STUNヘッダーの先頭からmessage integrity属性の直前の属性であるsoftware属性までとなります。
その後、STUNヘッダーのMessage Lengthの値を本来の値である72にして、CRC-32の値を計算します。
CRC-32の計算対象はfingerprint属性自体を除いたSTUNメッセージなので、message integrity属性までとなります。
TURNサーバーでは、このようにしてSTUNメッセージの属性を計算し、レスポンスのパケットを組み立てています。
おわりに
TURNサーバーをC#で作っている話や、TURNのプロトコルの仕組みを説明しました。TURNサーバーの開発を通して、これまで以上にパケットの気持ちがわかるようになった気がします。バイナリデータを一つ一つ読み解き、仕様通りの挙動を実装できたときは非常に楽しいです。
ですが、まだTURNサーバーは未完成です。これからも引き続き、TURNサーバーの実装やプロトコルの勉強を頑張っていきたいと思います。
以上、NTT Communications Advent Calendar 2021 12日目、「C# ( .NET Core )で作る自作TURNサーバー 〜開発のTipsとプロトコルの説明を添えて〜」でした!
それでは、明日もお楽しみに!!
参考サイト
rfc5389
rfc5766
rfc8656
[MS-TURN]: Message Header | Microsoft Docs
[MS-TURN]: Message Attribute | Microsoft Docs
Debugging | WebRTC for the Curious
*1:https://datatracker.ietf.org/doc/html/rfc5766#section-1
*2:https://webrtcforthecurious.com/docs/09-debugging/#networking-failure
*3:https://datatracker.ietf.org/doc/html/rfc5389#section-6
*4:https://datatracker.ietf.org/doc/html/rfc5389#section-15
*5:https://datatracker.ietf.org/doc/html/rfc5389#section-15.5
*6:https://datatracker.ietf.org/doc/html/rfc5389#section-15.4
*7:https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.hmacsha1?view=net-6.0