前回までのあらすじ。
- `nodertc/nodertc`を読んでた
- クライアントとSessionを確立する際に、内部的にいくつかサーバーを立ててた
- STUN
- DTLS
- SCTP
- こいつらの詳細を読み進めていく
というわけで、まずはSTUNから。
使われ方
- セッション確率の際に用意していたUDPソケットで初期化される
- 1000ms間隔で`STUN_BINDING_REQUEST`というメッセージを`send()`
- `STUN_EVENT_BINDING_REQUEST`を待ち受けて、`STUN_BINDING_RESPONSE`を`send()`
- あとはエラーもいちおうコンソールに出してる
というわけで基本的には`StunServer`の`send()`しか呼んでないのでそこを重点的に読みたい。
nodertc/stun
読んだバージョンは`1.3.0`です。
GitHub - nodertc/stun: Low-level Session Traversal Utilities for NAT (STUN) server
`src/index.js`が全てであり、たった90行!かと思いきや、`node_modules`というディレクトリをわざわざ用意してて、そこに依存がいろいろある。
全部の行数を総計すると、2192行もあった・・。
module.exports = { createMessage, createServer, validateFingerprint, validateMessageIntegrity, StunMessage, StunServer, constants, };
何はともあれ、これらを見ていく。
めぼしいクラスとしては`StunMessage`と`StunServer`くらい。
`createXxx()`系もこれらを使ってるだけ、あとは関数と定数なのでスルー。
class: StunServer
- `constructor()`
- `onMessage()`
- UDPソケットが受け取った`message`をさばくのが主
- 受け取ったパケットの先頭のbyteが`0..3` === STUNの時だけ、`process()`する
- `process()`
- この時に内部的に扱いたいように、`StunMessage`に変換
- `type`によって、異なるイベントを`emit()`
- `send()`
- `StunMessage`を`toBuffer()`して送る
- NodeRTCのセッションが持ってたUDPソケットの`send()`
UDPに流れてきたパケットのうち、STUNに関するものだけをリレーするのが仕事っぽい。
他に見落としてないなら、なんか冗長な印象。
class: StunMessage
- サーバーがやり取りするSTUNのパケットをいい感じにラップするもの
- `attribute`という単位で情報が付加されたり削られたり
- ここでは9種類定義されてる
- 内部的にバイナリを w/ スキーマで扱えるライブラリを使ってる
あらためて使われ方
- 1000ms間隔で`STUN_BINDING_REQUEST`というメッセージを`send()`
- `STUN_EVENT_BINDING_REQUEST`を待ち受けて、`STUN_BINDING_RESPONSE`を`send()`
この2つの先を追う。
- `send(STUN_BINDING_REQUEST)`される先は、接続してきたピアのUDPソケット
- なので中身はブラウザのWebRTC実装なはず
- それは追えないにしても、このサーバー側でも同等の実装をしているはず
- それが`STUN_EVENT_BINDING_REQUEST`のイベントを受けてからやってること
コードとしてはこう。
this.stun.on(STUN_EVENT_BINDING_REQUEST, (req, rinfo) => { assert(stun.validateFingerprint(req)); assert(stun.validateMessageIntegrity(req, this[_icePassword])); const userattr = req.getAttribute(STUN_ATTR_USERNAME); const sender = userattr.value.toString('ascii'); const expectedSender = `${this[_iceUsername]}:${this[_peerIceUsername]}`; assert(sender === expectedSender); const response = stun.createMessage( STUN_BINDING_RESPONSE, req.transactionId ); response.addAttribute( STUN_ATTR_XOR_MAPPED_ADDRESS, rinfo.address, rinfo.port ); response.addMessageIntegrity(this[_icePassword]); response.addFingerprint(); this.stun.send(response, rinfo.port, rinfo.address); });
というわけで、送られてきたものを検証して返事するのが仕事っぽい。
わかったこと
途中までそもそもこんなコードが必要な理由がわかってなかった。
そもそもP2Pが確立できてるなら、その後もSTUN(自分・相手の居場所を知るという意味で)の仕事なんてないのでは?と。
ただこれは勘違いで、STUNはSTUNでもICEが継続的に接続を検証するために必要な機構の方であり、RFCもあった。
RFC 7675 - Session Traversal Utilities for NAT (STUN) Usage for Consent Freshness
ちなみにログを仕込んでみたら、Chromeからは、2500ms間隔くらいで飛んできてて、Firefoxも5秒間隔でやってると書いてあった。
そのほか知り得たこと
- RFC 5389 - Session Traversal Utilities for NAT (STUN)
- 元となるRFCがあったが、紆余曲折あって範囲が小さくなったらしい
- RFC 3489 - STUN - Simple Traversal of User Datagram Protocol (UDP) Through Network Address Translators (NATs)
- 通称も"Simple Traversal of UDP through NAT"から"Session Traversal Utilities for NAT"に
- STUNの実装はサーバーとクライアントと別れる
- `stun:stun.l.google.com:19302`とか指定するやつで動いてるのがサーバーの実装
- クライアントの実装はブラウザのWebRTCが持ってる(はず)
- NodeRTCだとサーバー側の仕組はない(いらないから)
NodeJSで書かれたSTUNのサーバー、クライアントの実装はいくつかった。