Mbed TLSを使うようにしたlibdatachannelで、Mbed TLSのデバッグログを出力させる方法

はじめに

OBSとaiortcを使ったWHIPサーバーの間で通信をさせていたところ、途中でエラーになってしまう問題が発生しました。
その過程で色々と知見が得られたので、ブログ記事として記録しておこうと思います。

最終的に動いたコードはこちらです。

github.com

挑戦していたことと調査の流れ

OBSからWebRTC-HTTP ingestion protocol (WHIP)で映像を送り、aiortcを使ったサーバーでの受信に挑戦していました。
しかし、途中で「サーバーへの接続中に予期しないエラーが発生しました。詳細はログファイルを確認してください。」というダイアログが出て、通信が切断されてしまいました。
ログファイルを確認すると、 [obs-webrtc] [whip_output: 'adv_stream'] PeerConnection state is now: Failed というエラーログが記録されていました。

ここで、OBSのWebRTCプラグインがどのようなソフトウェアで構成されているかを簡単に紹介します。
WebRTCのライブラリには、libdatachannelが使用されています。
そして、SSL/TLSライブラリには、Mbed TLSが使われています。
libdatachannelは、GnuTLS、Mbed TLS、OpenSSLのいずれかを選んでビルドすることで、好みのSSL/TLSライブラリが利用できるようになっており、デフォルトではOpenSSLが使われます。
OBSは元々Mbed TLSを利用しており、それに合わせる形でlibdatachannelもMbed TLSを利用する設定でビルドされているものと思われます。

エラーログでは、PeerConnectionのstateが Failed になったと言っています。
しかし、これだけでは原因が分からないので、より詳細なログを出力するようにしてみます。

OBSにはログレベルを変更するような設定項目は無いため、ソースコードからビルドしていきます。
詳細なドキュメントがあるため、基本的にはドキュメントのとおりに作業すればビルドができます。
github.com
私の場合は、 cmake --preset macos でビルドするとXcodeでプロジェクトを開いた際にエラーになってしまったので、 cmake --preset macos -DCMAKE_OSX_ARCHITECTURES=x86_64 でビルドしました。
ビルドができることを確認したら、plugins/obs-webrtc/whip-output.cppの WHIPOutput::Setup() 内にログレベルを変更するコードを追加します。

github.com

PeerConnectionを作っている行の前に rtc::InitLogger(rtc::LogLevel::Debug); を追加すれば、ログレベルをデバッグに変更できます。
その後で再度ビルドして、同じ手順でWHIPでの配信を始めます。
実行すると、以下のようなエラーが出てきました。

ERROR [12608109] [rtc::impl::DtlsTransport::doRecv@595] DTLS recv: Handshake failed: SSL - A message could not be parsed due to a syntactic error
ERROR [12608109] [rtc::impl::DtlsTransport::doRecv@603] DTLS handshake failed
INFO  [12608109] [rtc::impl::PeerConnection::changeState@1166] Changed state to failed

エラーを見たところ、どうやらDTLSのハンドシェイクで失敗しているようです。

ここで、OBS特有の問題なのか、それ以外の環境でも起きうるのかの切り分けを行いました。
前述した通り、OBSが利用しているlibdatachannelはMbed TLSを利用する設定でビルドされたものなので、libdatachannelをMbed TLSを利用する設定でビルドして検証します。
検証をしやすくするため、libdatachannelをforkして、WHIPを使うサンプルコードを追加してみました。
github.com

libdatachannelにも、非常に丁寧なビルドの説明があるので、基本的にはこのとおりにコマンドを実行すればビルドができます。
github.com

Mbed TLSを使う場合は、CMakeLists.txtの USE_MBEDTLS をONにしてから、手順に沿ってビルドを行います。
github.com

ビルドができたら、手順に沿ってWHIPのサンプルを実行します。
github.com

このコードを実行すると、OBSと同じエラーが出ることが分かります。
したがって、このエラーはOBS特有の問題ではなく、libdatachannel、Mbed TLS、またはaiortcに原因がある可能性が高いことが分かりました。

次に、どこでエラーになっているかを調べます。

エラーメッセージにある A message could not be parsed due to a syntactic error という文章でMbed TLSソースコードを検索すると、以下の行がヒットします。
github.com
つまり、Mbed TLSで定義されている MBEDTLS_ERR_SSL_DECODE_ERROR が返されたことが分かります。
また、ブレークポイントを置いたり、printfデバッグをすると、libdatachannelの以下の行で mbedtls_ssl_handshake が呼ばれた際にエラーになっていることが分かります。
github.com

以上を踏まえ、Mbed TLSのmbedtls_ssl_handshake関数を追っていって、 MBEDTLS_ERR_SSL_DECODE_ERROR が返される箇所を調べれば、原因が分かりそうです。
処理の流れを追っていくと、以下のいずれかの関数において、 MBEDTLS_ERR_SSL_DECODE_ERROR が返されていることが分かります。

  • mbedtls_ssl_parse_sig_alg_ext
  • ssl_parse_certificate_chain
  • mbedtls_ssl_parse_finished

ただ、エラーが返される箇所は複数あるので、ここから原因を追っていくのはちょっと大変そうです。

libdatachannelのコードを変更して、Mbed TLSのログを出力させる

タイトル回収です。

まず、libdatachannelのsrc/impl/tls.hppに #include "mbedtls/debug.h" のincludeを追加します。

次に、src/impl/dtlstransport.cppの375行目付近に以下のコードを追記します。

// ref: https://github.com/Mbed-TLS/mbedtls/blob/development/programs/ssl/dtls_client.c#L71
static void my_debug(void *ctx, int level, const char *file, int line, const char *str) {
	((void)level);

	switch (level) {
	case 0:
		PLOG_NONE << file << ":" << line << ": " << str;
		break;
	case 1:
		PLOG_ERROR << file << ":" << line << ": " << str;
		break;
	case 3:
		PLOG_INFO << file << ":" << line << ": " << str;
		break;
	case 2:
	case 4:
		PLOG_DEBUG << file << ":" << line << ": " << str;
		break;
	default:
		break;
	}
}

最後に、 DtlsTransport::DtlsTransport 内のmbedtls_ssl_conf_rng関数を呼んでいる直後くらいに、以下の2行を追加します。

mbedtls_ssl_conf_dbg(&mConf, my_debug, stdout);
mbedtls_debug_set_threshold(4);

追記したら、再度libdatachannelをビルドし、同じ手順でWHIPのサンプルを実行します。
実行すると、以下のようなエラーログが得られました。

INFO  [12639790] [rtc::impl::my_debug@394] /tmp/mbedtls-20231015-5566-1sh6mzm/mbedtls-mbedtls-3.5.0/library/ssl_tls12_server.c:1064: 0x7fe0348097c8: client hello v3, handshake type: 1
INFO  [12639790] [rtc::impl::my_debug@394] /tmp/mbedtls-20231015-5566-1sh6mzm/mbedtls-mbedtls-3.5.0/library/ssl_tls12_server.c:1073: 0x7fe0348097c8: client hello v3, handshake len.: 245
ERROR [12639790] [rtc::impl::my_debug@390] /tmp/mbedtls-20231015-5566-1sh6mzm/mbedtls-mbedtls-3.5.0/library/ssl_tls12_server.c:1088: 0x7fe0348097c8: bad client hello message: 243 != 12 + 245
INFO  [12639790] [rtc::impl::my_debug@394] /tmp/mbedtls-20231015-5566-1sh6mzm/mbedtls-mbedtls-3.5.0/library/ssl_tls.c:3942: 0x7fe0348097c8: <= handshake
ERROR [12639790] [rtc::impl::DtlsTransport::doRecv@628] DTLS recv: Handshake failed: SSL - A message could not be parsed due to a syntactic error
ERROR [12639790] [rtc::impl::DtlsTransport::doRecv@636] DTLS handshake failed
INFO  [12639790] [rtc::impl::PeerConnection::changeState@1179] Changed state to failed

bad client hello message: 243 != 12 + 245 というエラーメッセージが気になりますね。
Mbed TLSのコードを検索すると、以下の行がヒットします。
github.com
書かれているコメントを読むと、どうやらMbed TLSは、ClientHelloのフラグメンテーションを(まだ)サポートしていないようです。

WiresharkでDTLSのパケットをキャプチャーしてみると、たしかにClientHelloのフラグメンテーションが起きているようです。
これで無事原因が特定できました。🎉

動かすためには

動かす方法は、試行錯誤をしていてたまたま見つけました。

aiortcのDTLS関連のコードをいろいろいじっていたところ、以下の行の暗号スイートの設定をコメントアウトすると動くことに気づきました。
github.com
元々の設定は HIGH:!CAMELLIA:!aNULL で、このときのClientHelloをWiresharkで見てみると、暗号スイートの数は56個でした。
一方、コメントアウトした場合は28個に減っており、フラグメンテーションも起きていませんでした。
デフォルトの暗号スイートの数が少ないため、ClientHelloがフラグメンテーションされないサイズに収まっていることが原因と思われます。
Mbed TLSがClientHelloのフラグメンテーションに対応していない以上、対処方法はClientHelloのフラグメンテーションを防ぐことしかありません。
今回は、暗号スイートの数を絞ることで、フラグメンテーションを防ぐことにしました。

本来であればaiortcにPRを送ってマージしてもらい、リリースされるのを待つのが良いのかもしれませんが、今回はすぐに動かしたかったので、パッチを当てる方法で回避することにしました。
github.com
このパッチによって、暗号スイートの数は9つとなり、ClientHelloのフラグメンテーションも回避できました。

暗号スイートはハードコーディングするのではなく、設定できるようなインターフェースを作るのが良さそうかなと思うので、後でaiortcにPRを出してみようかなと思います。

おわりに

普段は公私ともTypeScriptをよく書いているので、C/C++デバッグは大変でした。
ただ、OBS、libdatachannel、Mbed TLSはいずれもしっかりとドキュメントが書かれており、コードも(たぶん)見やすいと感じたので、私でもなんとか原因の特定ができました。
また、ChatGPTやGitHub Copilotにもだいぶ助けられました。AIの力がなければ、動くC++のコードは書けなかったと思います。

めちゃくちゃ頭を使ったので疲れましたが、エラーがちゃんと出て原因の特定まで持っていけたので、とても勉強になりました。