そもそも「STUNを実装する is 何」というところから整理しないとですが、しました。
ただしタイトルにもある通り、一部です。
GitHub - leader22/webrtc-stun: 100% TypeScript STUN implementation for WebRTC.
JavaScriptの先行実装はいろいろ見つかるけど、TypeScriptの実装はたぶん初。
`webrtc-stun`というパッケージ名で、npmからもインストールできます。
そもそもSTUNとは
- Session Traversal Utilities for NAT の略
- WebRTCでピア同士がつながるために、NATの向こう側にいる相手のIPやらが必要
- それをどうにかして取ってくるための方法がまとまってる仕様
- 手続きとそれに使うツールの作り方が書いてある
- 簡単に言うと、固定長のヘッダ + 任意の数の属性からなるメッセージを送り合う決まりになってる
- RFCのURLはこちら
- 元々はRFC3489で定義されてて、classic STUNと称されたりする
ただこのRFCはあくまでSTUNそれ自体の説明書であって、ただの辞書です。
WebRTCにおいてどういう用途で使うとかはほぼ書いてなくて、これがWebRTCスタックを実装する時にいちばんつらいところらしい。
しかも別のRFCでしれっと拡張されてたりもするので、全容が把握できなくていい感じにコード書くのつらい。まじつらい。
WebRTCにおけるSTUNの用途
このあたりは、主にはICEとして別のRFCにまとまってる。
- Interactive Connectivity Establishment
- 用途はいくつかあって、随所にSTUNが使われてる
- 早速さっきのRFCを拡張しますとか書いてあったりもする
- もちろんこの他のRFCでも使われたり拡張されたりするけど
- DTLSとか
- TURNとか
ともあれ、今回実装したのはあくまで、
- RFC8445に書いてあるユースケースのうち
- RFC5389に書いてある仕様だけで完結する部分
- と余力のある限り他のRFC5389に書いてある部分
です。
で、具体的な用途として使えるのは、`icecandidate`の候補収集。
ICEの候補収集
SDPに載る`a=candididate`の行の話。
`RTCPeerConnection`の`iceServers`でオプションを渡すと、ICEの経路収集でそのSTUNサーバーが使われるようになる。
const pc = new RTCPeerConnection({ sdpSemantics: 'unified-plan', // コレを指定すると iceServers: [{ urls: 'stun:stun.l.google.com:19302' }], }); pc.createDataChannel(''); pc.createOffer() .then(s => pc.setLocalDescription(s)); // ココが多く発火する pc.onicecandidate = ev => { if (ev.candidate !== null) { console.log(ev.candidate.candidate); } else { console.log(pc.localDescription.sdp); } }
このオプションの有無で、`onicecandidate`が発火する回数が変わるし、指定してた場合は、`srflx`っていう`candidate-type`がついた経路が取得できるはず。
つまりブラウザの中にSTUNのクライアントの実装がいて、`setLocalDescription()`のタイミングで指定されたSTUNサーバーに問い合わせ(`BINDING-REQUEST`)をしてる。
STUNサーバーはそれを受けてあなたのIPやらポートはこうですという返事(`BINDING-RESPONSE`)をする、その返事の中で`XOR-MAPPED-ADDRESS`という属性を付与して返すのが決まり。
その結果、ブラウザがそれを`candidate`としてSDPに書くという流れ。
ちなみに、ブラウザではなくサーバーで動かすWebRTCの実装をする場合は、そもそも別のやりかたでグローバルなIPがわかってたりもするはずで、この用途においてはSTUNを使う必要がなかったりもする。
BINDING-REQUESTを送る
実装したSTUNを使って、実際にリクエストを送信するコードがこちら。
const dgram = require('dgram'); const { StunMessage } = require('webrtc-stun'); const socket = dgram.createSocket({ type: 'udp4' }); socket.on('message', msg => { const res = StunMessage.createBlank(); // if msg is valid STUN message if (res.loadBuffer(msg)) { // if msg is BINDING_RESPONSE_SUCCESS if (res.isBindingResponseSuccess()) { const attr = res.getXorMappedAddressAttribute(); // if msg includes attr if (attr) { console.log('RESPONSE', res); console.log(attr.ip, attr.port); } } } socket.close(); }); const req = StunMessage.createBindingRequest(); console.log('REQUEST', req); socket.send(req.toBuffer(), 19302, 'stun.l.google.com');
やってることはコード見たらわかるくらい単純なので割愛。
今回はUDPでSTUNメッセージをやり取りするので、`dgram`を使うくらい。
WebRTC的にはTCPを使うこともあるらしい。
レスポンス例
インターネットに公開されてるSTUNサーバーはいくつもあって、上のコード例で使ってるGoogleの他にもいろいろある。(検索するとリストいっぱいでてくる)
`BINDING-REQUEST`を送った場合、`BINDING-RESPONSE`で返ってくる属性はこんな感じだった。
- `stun.l.google.com:19302`
- RFC5389: XOR-MAPPED-ADDRESS
- `stun.webrtc.ecl.ntt.com:3478`
- RFC5389: XOR-MAPPED-ADDRESS
- RFC5389: MAPPED-ADDRESS
- RFC5389: SOFTWARE
- RFC5389: FINGERPRINT
- RFC5780: RESPONSE-ORIGIN
`XOR-MAPPED-ADDRESS`だけあれば、この用途としては十分だということがわかる・・。
その他は返ってきても使わないので。
レスポンスには一応`SUCCESS`と`ERROR`があるけど、`SUCCESS`を返すに値しないリクエストは無視する!っていう挙動のSTUNサーバーも割といました。
BINDING-RESPONSEを返す
さっきのJavaScriptのコードの`iceServers`の部分を、自分で実装したSTUNに向ければもちろん試すことができる。
const dgram = require('dgram'); const { StunMessage } = require('webrtc-stun'); const socket = dgram.createSocket({ type: 'udp4' }); socket.on('message', (msg, rinfo) => { const req = StunMessage.createBlank(); // if msg is valid STUN message if (req.loadBuffer(msg)) { // if STUN message has BINDING_REQUEST as its type if (req.isBindingRequest()) { console.log('REQUEST', req); const res = req .createBindingResponse(true) .setXorMappedAddressAttribute(rinfo); console.log('RESPONSE', res); socket.send(res.toBuffer(), rinfo.port, rinfo.address); } } }); socket.bind(55555);
サーバーなのでポートを`bind()`してる。
UDPのメッセージを受けると`RemoteInfo`がついてるので、それを送り返すだけの簡単なお仕事。
ChromeもFirefoxもSafariも、この用途に関しては何の属性もつけてこない模様。
1つ注意するとすれば、`localhost`でSTUNサーバーを立ててしまうと、そもそも`host`のネットワークと一緒やんけ!ってことで無視されてSDPに載らないです。
その他の用途
RFC5389の範囲をこえた用途の一例 = RFC8445に書いてある例を書いとく。
だいたいが収集された`candidate`から最終的な経路が選ばれて接続を確立したあとに必要な用途で、Keep AliveとかConsent FreshnessとかConnectivity Checksとか定義されてる。
ちなみにこれらの挙動はテキトーなSDPをでっちあげてブラウザに渡すと確認できて、新たな属性にも出会えます・・。
属性の名前と、出自RFCはこんな感じ。
- RFC5389: USERNAME
- RFC5389: MESSAGE-INTEGRITY
- RFC8445: PRIORITY
- RFC8445: USE-CANDIDATE
- RFC8445: ICE-CONTROLLING
- RFC5389: FINGERPRINT
- NETWORK-INFO
いやー奥が深まりますね!
STUN = RFC5389を実装するためには、そもそもの用途を規定するRFC8445を理解しないといけないという。
webrtc-stunについて
いちおうWebRTCにおけるSTUNの用途の必要なところだけを実装するつもりで、このリポジトリを`webrtc-stun`と名付けて開発してたんですが、上述の通りRFC5389だけではそれをカバーできてないです。
なのでAPIもこの構成がベストかどうかまだ判断しかねる状態で、他のRFCを実装してたらまたまるっと書き直すとかも普通にあるのでは・・?という感じ。
この先を追って実装するかはまだ未定ですが、なんか進捗あればまた書きます。
やるとしたらロードマップはこんな感じ。
- [x] RFC5389: candidate収集に必要な部分
- [ ] RFC5389: その他の属性
- いま4/10だけ実装した
- [ ] さらに他のRFCで拡張されてるもの
- 全容は不明
割と長い道のりですよね・・!
ちなみにこのへん全部実装してあるのが、この間まで読んでたNodeRTCのSTUN実装です。
GitHub - nodertc/stun: Low-level Session Traversal Utilities for NAT (STUN) server
まじすごい。