WebRTC は、Google が 2011 年にリリースしたオープンソースのメディア フレームワークです。ブラウザ内でリアルタイムのオーディオおよびビデオ通信を実行できます。P2P 通信用に設計されています。開発者は、通信の一端として独自のサーバーを構築することもできます。ネットワーク条件が厳しい次のシナリオでは、通信を直接確立できず、中継サーバー TURN (NAT を使用したリレーを使用したトラバーサル) の助けを借りて転送する必要があります。
- 一方の端は対称 NAT で、もう一方の端はポート制限されたコーン NAT または対称 NAT であるため、P2P を確立できません。このツールは独自のネットワークの NAT タイプを検出できますhttps://github.com/aarant/pynat
- 銀行や政府機関など、ネットワーク下りに厳しい制限がある環境では、アクセスが必要な外部ネットワーク IP アドレスをゲートウェイ ホワイトリストに追加する必要があります。TURN サーバーは独立して展開でき、TURN のパブリック IP アドレスをホワイトリストに追加できます。
- 非常に厳格なセキュリティ要件を持つファイアウォールでは UDP 通信が許可されず、ポート 443 トラフィックを介した TLS のみが許可されます。
TURNプロセス分析
プロトコルは 3 つの部分に分かれています: 1.
TURN サーバー上に 送信リソース
を作成します (割り当てと呼ばれます) 2. データを送信するための表示モード
3. データを送信するためのチャネル モード
これら 2 つのデータ送信方法は並行していることに注意してください。 。3 部構成のプロセス シーケンス図は次のとおりです (画像リンク https://justme0.com/assets/pic/turn/seq.svg )。クライアントは WebRTC コードを参照し、サーバーは pion/turn コードを参照します。 。
1. 割り当てリソースの作成
allocation は、TURN サーバーによってクライアントに割り当てられるリソースです。データ構造は次のとおりで、主なフィールドは次のとおりです ( https://github.com/pion/turn/blob/master/internal/allocation/allocation を参照)。詳細については#L23 を参照してください )。5 つ組の FiveTuple <clientIP, clientPort, svrIP, svrPort, protocol> で割り当てを識別します。プロトコルの最新の RFC ドキュメントでは、TCP/UDP/TLS/DTLS が規定されています。Pion はまだ DTLS をサポートしていません。サーバーのリスニング ポート svrPort のデフォルトは 3478 です。 TCP/UDP の場合は 5349、TLS/DTLS の場合は 5349。
// FiveTuple is the combination (client IP address and port, server IP
// address and port, and transport protocol (currently one of UDP,
// TCP, or TLS)) used to communicate between the client and the
// server. The 5-tuple uniquely identifies this communication
// stream. The 5-tuple also uniquely identifies the Allocation on
// the server.
type FiveTuple struct {
Protocol
SrcAddr, DstAddr net.Addr
}
type Allocation struct {
RelayAddr net.Addr
Protocol Protocol
TurnSocket net.PacketConn
RelaySocket net.PacketConn
fiveTuple *FiveTuple
permissionsLock sync.RWMutex
permissions map[string]*Permission
channelBindingsLock sync.RWMutex
channelBindings []*ChannelBind
lifetimeTimer *time.Timer
}
割り当て構造
データ構造内の権限フィールドと channelBindings フィールドに特に注意してください。権限のキーはピア アドレスであり、channelBindings は配列であり、これもピア アドレスによって識別されます。以下にシーケンス図を用いて流れを説明します。
1.1 STUNバインドリクエスト
STUN 関数と同様に、クライアントの IP とポートを返します。これは、自身のエクスポート アドレスの終了を通知し、ローカルの候補を収集するために使用されます。サーバーはステートレスです。
1.2 割り当て要求
リソースの割り当てをリクエストします。リクエスト パラメータは、TURN とピアの間で UDP と TCP のどちらであるかを識別します。WebRTC クライアントは UDP をハードコードします。RFC ドキュメントは仕様であり、WebRTC は実装であることに注意してください。標準で指定されているすべての機能が実装されているわけではありません。以下のソース コードからわかるように、リクエストには MAC コード AttrMessageIntegrity が含まれておらず、401 エラー コード CodeUnauthorized が返され、レルムと乱数が返されます。ここでのアンチエラーは通常のプロセスであり、サーバーのいくつかのパラメーターを最後まで持ち込むことです。
func authenticateRequest(r Request, m *stun.Message, callingMethod stun.Method) (stun.MessageIntegrity, bool, error) {
respondWithNonce := func(responseCode stun.ErrorCode) (stun.MessageIntegrity, bool, error) {
nonce, err := buildNonce()
if err != nil {
return nil, false, err
}
// Nonce has already been taken
if _, keyCollision := r.Nonces.LoadOrStore(nonce, time.Now()); keyCollision {
return nil, false, errDuplicatedNonce
}
return nil, false, buildAndSend(r.Conn, r.SrcAddr, buildMsg(m.TransactionID,
stun.NewType(callingMethod, stun.ClassErrorResponse),
&stun.ErrorCodeAttribute{Code: responseCode},
stun.NewNonce(nonce),
stun.NewRealm(r.Realm),
)...)
}
if !m.Contains(stun.AttrMessageIntegrity) {
return respondWithNonce(stun.CodeUnauthorized)
}
...
}
応答を受信した後、端末にレルムと乱数を設定し、MAC コード = MD5(ユーザー名 ":" レルム ":" SASLprep(パスワード)) を計算します。MAC コードは次のステップで伝えられます。TurnAllocateRequest を参照してください。 :: OnAuthChallenge https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/p2p/base/turn_port.cc;l=1439
1.3 2 回目の割り当てリクエスト
端末は、前のステップで返された MAC コード、レルム、および乱数を伝送し、再度割り当てを要求し、サーバーは MAC コードを検証し、ok の後に割り当てデータ構造を作成し、タイマーを lifetimeTimer フィールドに割り当てます。生存時間のデフォルトは 10 分です。WebRTC 端末は、生存を維持するために 1 分前にハートビートを送信します。つまり、9 分ごとにハートビートを送信します。クライアントがリソースを解放したい場合は、リフレッシュ要求パラメータのライフタイムに 0 を入力します。これについては次のセクションで説明します。リソースを割り当てた後、サーバー プロセスはピアと通信するためにUDP ポートをバインドし、for ループはピアからのパケットの受信を待機します。
func (m *Manager) CreateAllocation(fiveTuple *FiveTuple, turnSocket net.PacketConn, requestedPort int, lifetime time.Duration) (*Allocation, error) {
...
go a.packetHandler(m)
...
}
// 从relay端口(UDP)收包处理
func (a *Allocation) packetHandler(m *Manager) {
buffer := make([]byte, rtpMTU)
for {
n, srcAddr, err := a.RelaySocket.ReadFrom(buffer)
if err != nil {
m.DeleteAllocation(a.fiveTuple)
return
}
a.log.Debugf("relay socket %s received %d bytes from %s",
a.RelaySocket.LocalAddr().String(),
n,
srcAddr.String())
...
}
}
1.4 割り当てリフレッシュ要求
リクエストパラメータに指定した期間(つまりexpire)を指定してリソースを解放しますが、0を渡した場合はリソースを即時に解放し、expireは最後に定期的に更新する、つまり定期的に生き続けることを意味します。
2. データ送信方法の指定
RFCによる表示方法の概要
2.1 作成権限
表示モードでデータを転送する前に、まず許可をリクエストしてください。端末にリモート候補を追加すると、TurnEntry オブジェクトが作成され、作成許可リクエストがコンストラクターで送信され、ピア アドレスがリクエスト パラメーターで運ばれます。
TurnEntry::TurnEntry(TurnPort* port, Connection* conn, int channel_id)
: port_(port),
channel_id_(channel_id),
ext_addr_(conn->remote_candidate().address()),
state_(STATE_UNBOUND),
connections_({conn}) {
// Creating permission for `ext_addr_`.
SendCreatePermissionRequest(0);
}
https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/p2p/base/turn_port.cc;l=1764より
許可キャプチャパケットを作成する
サーバーはシグナリングを受信すると、割り当て構造のアクセス許可マップに kv を追加します (キーはピア アドレスであり、アクセス許可を識別し、同時にタイマーを作成します。タイムアウトになった場合は、アクセス許可を削除します)。タイムアウト時間は5分で、最後に最後までパケットを返します。
応答パケットを受信した後、端末は生存を維持するために次のハートビートを送信する準備が整います。
void TurnEntry::OnCreatePermissionSuccess() {
RTC_LOG(LS_INFO) << port_->ToString() << ": Create permission for "
<< ext_addr_.ToSensitiveString() << " succeeded";
if (port_->callbacks_for_test_) {
port_->callbacks_for_test_->OnTurnCreatePermissionResult(
TURN_SUCCESS_RESULT_CODE);
}
// If `state_` is STATE_BOUND, the permission will be refreshed
// by ChannelBindRequest.
if (state_ != STATE_BOUND) {
// Refresh the permission request about 1 minute before the permission
// times out.
TimeDelta delay = kTurnPermissionTimeout - TimeDelta::Minutes(1);
SendCreatePermissionRequest(delay.ms());
RTC_LOG(LS_INFO) << port_->ToString()
<< ": Scheduled create-permission-request in "
<< delay.ms() << "ms.";
}
}
https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/p2p/base/turn_port.cc;l=1846より
2.2 表示
端末からサーバへデータを送信するシグナリングをセンドインディケーションといい、サーバから端末へ送信するシグナリングをデータインディケーションといいます。(この名前は対応が悪いような気がします)
送信インディケーションのオーバーヘッドは、ピアアドレス、つまり誰に送信するかを指定するチャネルの36Bよりも大きくなります。サーバーはピアのアドレスに基づいて許可が存在するかどうかを確認し、許可が存在する場合は、伝送されているデータを取り出してピアに送信します。
指示キャプチャパケットを送信する
ピアのパケットを受信するとき、チャネル モードが確立されているかどうかを確認します。チャネル モードが確立されている場合は、チャネル モードが優先されます。確立されていない場合は、許可モードを使用してエンドに送り返します。データ表示構造は、送信指示と同じです。
上記の送信方法を読んで、何か問題があると思いますか?
権限の作成時にクライアントがピア アドレスを任意に指定できることに注意してください。サービスがイントラネット上に展開されている場合、SSRF (サーバーサイド リクエスト フォージェリ) の脆弱性と同様に、ユーザーが悪意を持ってイントラネット サーバーをスキャンする可能性があります。このレポートを参照してください。 https://hackerone.com /reports/333419?from_wecom=1
攻撃者は、TURN 接続メッセージ (メソッド `0x000A` 、 https:// tools.ietf.org/html /rfc6062#section-4.3 ) をプライベート IPv4 アドレスに変換します。
UDP パケットは、TURN 送信メッセージ指示 (メソッド `0x0006` 、 https://tools.ietf.org/html/rfc5766#section-10 )で `XOR-PEER-ADDRESS` をプライベート IP に設定することによってプロキシできます
。たとえば、イントラネット上のサーバーがイントラネット用の HTTP サービスを提供する場合、リソースの作成時に TURN とピアの間の TCP メソッドを指定し (pion/turn はサポートせず、coturn はサポートします)、その後、許可と送信指示を作成するときにピアを指定します。 address はイントラネット アドレス (総当たり枯渇) で、HTTP リクエストは TURN プロトコルでラップされ、イントラネット サーバー上のデータが取得されます。
次章で紹介する送信方法でもこの問題はありますが、WebRTC の転送のみで、WebRTC が UDP のみを使用する場合は、TURN による TCP 中継ポートの割り当て機能を無効にして、TCP 中継ポートをブロックすることで解決できます。一般的に使用されるプロトコルの UDP ポートピア アドレスを確認するには、元の投稿で著者が述べたことを詳しく読んでください。
3. データを送信するチャネル方法
RFC のチャネル メソッドの概要
3.1 チャネルバインドリクエスト
パーミッション メソッドと同様に、データを送信する前にチャネルの作成を要求します。TurnEntry オブジェクトが実際のデータを送信するときにチャネルの作成を要求します。以下のコード、キーワード SendChannelBindRequest を参照してください。
int TurnEntry::Send(const void* data,
size_t size,
bool payload,
const rtc::PacketOptions& options) {
rtc::ByteBufferWriter buf;
if (state_ != STATE_BOUND ||
!port_->TurnCustomizerAllowChannelData(data, size, payload)) {
// If we haven't bound the channel yet, we have to use a Send Indication.
// The turn_customizer_ can also make us use Send Indication.
TurnMessage msg(TURN_SEND_INDICATION);
msg.AddAttribute(std::make_unique<StunXorAddressAttribute>(
STUN_ATTR_XOR_PEER_ADDRESS, ext_addr_));
msg.AddAttribute(
std::make_unique<StunByteStringAttribute>(STUN_ATTR_DATA, data, size));
port_->TurnCustomizerMaybeModifyOutgoingStunMessage(&msg);
const bool success = msg.Write(&buf);
RTC_DCHECK(success);
// If we're sending real data, request a channel bind that we can use later.
if (state_ == STATE_UNBOUND && payload) {
SendChannelBindRequest(0);
state_ = STATE_BINDING;
}
} else {
// If the channel is bound, we can send the data as a Channel Message.
buf.WriteUInt16(channel_id_);
buf.WriteUInt16(static_cast<uint16_t>(size));
buf.WriteBytes(reinterpret_cast<const char*>(data), size);
}
rtc::PacketOptions modified_options(options);
modified_options.info_signaled_after_sent.turn_overhead_bytes =
buf.Length() - size;
return port_->Send(buf.Data(), buf.Length(), modified_options);
}
https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/p2p/base/turn_port.cc;l=1846より
作成リクエストには重要なフィールド チャネル番号があります。端末が提供するこのチャネルは生成・識別され、チャネル番号の値が規定されている
・0x0000~0x3FFF:チャネル番号として使用不可 ・
0x4000~0x7FFF:チャネル番号として使用可能な値(16383)
・0x8000 -0xFFFF: 予約値、将来の使用のために予約されています
チャネルバインド要求パケットキャプチャ
作成リクエストを受信した後、サーバーは Allocation 構造体の channelBindings 配列に項目を追加し、チャネル番号またはピア アドレスでチャネルを識別し、タイムアウト 10 分のタイマーを作成します。チャネルが作成されている場合は、更新の有効期限が切れます。さらに、バインド要求により、権限の有効期限が更新されます。最後にパッケージを最後まで戻します。
戻りパケットを受信した後、端末は存続するために次のハートビートの準備も整い、ハートビートはタイムアウトの 1 分前に更新されます。
void TurnChannelBindRequest::OnResponse(StunMessage* response) {
RTC_LOG(LS_INFO) << port_->ToString()
<< ": TURN channel bind requested successfully, id="
<< rtc::hex_encode(id())
<< ", code=0" // Makes logging easier to parse.
", rtt="
<< Elapsed();
if (entry_) {
entry_->OnChannelBindSuccess();
// Refresh the channel binding just under the permission timeout
// threshold. The channel binding has a longer lifetime, but
// this is the easiest way to keep both the channel and the
// permission from expiring.
TimeDelta delay = kTurnPermissionTimeout - TimeDelta::Minutes(1);
entry_->SendChannelBindRequest(delay.ms());
RTC_LOG(LS_INFO) << port_->ToString() << ": Scheduled channel bind in "
<< delay.ms() << "ms.";
}
}
https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/p2p/base/turn_port.cc;l=1732より
3.2チャンネルデータ
前のセクションの実データを送信するコード TurnEntry::Send() からわかるように、データ送信時にチャネル モードが作成されている場合、データはチャネル モードで送信されます (つまり、優先チャネル モードが送信されます)。 )、そのオーバーヘッドはわずか 4B であり、表示モードと比較してオーバーヘッドがはるかに少なくなります。
チャネルデータパケットキャプチャ
上記 2 つの送信方法を読んで質問ですが、チャネル方式はパーミッション方式に比べてオーバーヘッドが少なく、ハートビート送信時にパーミッションの有効期限も更新できます。 . チャネル メソッドのみを使用することはできますか? 最初 ( TurnEntry コンストラクター) では、作成権限の代わりにチャネル バインドを使用しますか? この質問は stackoverflow https://stackoverflow.com/questions/75611078/why-not-use-only-channel-data-in-webrtc-turn-client
で尋ねられ 、WebRTC の責任者が ICE RFC 5245 ドキュメントについて言及しました。 ICE プロセスが完了した後にチャネルを作成すること、つまり、候補ペアを選択した後にチャネルを作成することをお勧めします。実際、TURN ドキュメントでは、伝送される特定のデータについてはまったく考慮されていません。候補の概念は、ICE ドキュメントで推奨されている WebRTC ICE に属します。ネイティブ側は、チャネルのみを使用するように最適化および変更できると思います方法。最後に、要約すると、リクエスト クラスのプロトコル バックグラウンドは、割り当てリクエストとリフレッシュ リクエスト、作成許可リクエスト、およびチャネル バインド リクエストを含むMAC コードを認証する必要があります。TURN には本文の 3 つの部分に対応する 3 種類のタイマーがあり、端末は定期的に生存するために「ハートビート」を送信する必要があり、許可はチャネル バインドによって維持できます。
オリジナルのWEBRTC TURNプロトコルのソースコード解析 - ナレッジ
★記事末尾の名刺では、オーディオ・ビデオ開発学習教材(FFmpeg、webRTC、rtmp、hls、rtsp、ffplay、srs)やオーディオ・ビデオ学習ロードマップ等を無料で受け取ることができます。
以下を参照してください!