console.lealog();

@leader22のWeb系に関する勉強めもブログですのだ

NodeJS製WebRTC DataChannel、NodeRTCのコードを読む Part.2

前回までのあらすじ。

  • `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()`
    • NodeRTCのセッションが持ってたUDPソケットを引数にインスタンス
    • extends `EventEmitter`
  • `onMessage()`
    • UDPソケットが受け取った`message`をさばくのが主
    • 受け取ったパケットの先頭のbyteが`0..3` === STUNの時だけ、`process()`する
  • `process()`
    • この時に内部的に扱いたいように、`StunMessage`に変換
    • `type`によって、異なるイベントを`emit()`
  • `send()`
    • `StunMessage`を`toBuffer()`して送る
    • NodeRTCのセッションが持ってたUDPソケットの`send()`

UDPに流れてきたパケットのうち、STUNに関するものだけをリレーするのが仕事っぽい。

他に見落としてないなら、なんか冗長な印象。

class: StunMessage

あらためて使われ方

  • 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秒間隔でやってると書いてあった。

ICE connected or not... - Advancing WebRTC

ちなみに、他のOSSの実装には、これよりもっと薄いやつもある。

  • サーバーに対してクライアントが`Binding`リクエストを送る
  • それを受けて`STUN_ATTR_XOR_MAPPED_ADDRESS`なる`attribute`を付加してレスポンスする

この2つだけあれば、事足りるっぽい。

そのほか知り得たこと

NodeJSで書かれたSTUNのサーバー、クライアントの実装はいくつかった。

読んでみて

  • コードは本体より読みにくい
    • モジュールをまたがってやり取りされるオブジェクト、型がないので追いかけるのつらい
    • `options`とか言われてもなんやっけ・・ってなる
  • STUNはSTUNでも、そういうSTUNではなかった

やはりRFCを読むのが先か?感が強まってきたけど、やり取りの仕様(フォーマット)としてのRFCもあれば、その仕様をなぜ使うかどう使うに触れてるRFCもあって、それぞれが体系的にどういう関係性なのかを最初に知らないと、筋道立てて調べられないことに気付いた・・。

まぁひとまず、当初の予定通りNodeRTCのDTLSサーバーの部分を読んでいきます。