🧊

WebRTCでTURNを使う

トピックとしてはまったく目新しくないけど、そういやまとめてないなと思ったのでメモ。

TURN

RFC 5766 - Traversal Using Relays around NAT (TURN): Relay Extensions to Session Traversal Utilities for NAT (STUN)

`Traversal Using Relays around NAT`ってやつ。

P2Pの場合、各エンドポイントにはそれぞれの都合があって、簡単につなげないこともある。

ざっくり言うと、たとえば特定のプロトコルが使えないとか、ポートが空いてないとか、変なプロキシがいるとか。

そんなときにいったんグローバルにいるTURNサーバーを中継することで、厳密なP2Pではなくなるけど、接続成功率が上がったり、通信経路を固定化できたり、場合によっては通信の品質も安定したりする・・スルメ系なやつ。

まあそんな話はさておき、これをJSのAPIからどう指定して、どう扱うか。

RTCIceServer

`RTCIceServer`というオブジェクトでもって、`RTCConfiguration`ってオブジェクトに指定する。

const iceServers = [
  // STUNサーバーを使う場合も
  { urls: "stun:stun1.example.net" },
  // TURNサーバーを使う場合も
  {
    urls: "turns:turn3.example.net",
    username: "12334939:fred",
    credential: "adfsaflsjflds"
  }
];

const pc = new RTCPeerConnection({ iceServers });

// ...

という具合に、複数のサーバーが指定できる。

TURNには、なんらかの方法で払い出した`username/credential`が必要。
だってこれがないとタダ乗りされちゃうから・・・。

ちなみにこうすればTURNを必ず経由するかというと、そうではない。
今のままでは接続経路の候補に入るだけで、絶対に使われる保証はない。(他の経路でつながるなら、そもそもTURNは不要なので、そういう意味では妥当)

const pc = new RTCPeerConnection({
  iceServers,
  iceTransportPolicy: "relay"
});

こうすると、TURN経由の経路だけが接続候補になる。

まず最初に1発つなげるという意味では、これだけでOK。

TURN as a Service

WebRTCでWebのサービスやってると、やっぱりTURNのcredentialもRESTで提供したくない?したいよね?ってなる。
それこそ、TURN as a Serviceとか。

ただTURNのサーバーはWebサーバーとは別にしたいし、認証はしたいけど、なんか疎に連携するいい方法はないじゃろか?ってなる。

そして生まれたのがこのドラフト。

draft-uberti-rtcweb-turn-rest-00 - A REST API For Access To TURN Services

RESTサーバーとTURNサーバーで共有のシークレットキーを持っておいて、それを使って得たハッシュを使って認証する作戦。
ユーザー名にはタイムスタンプを入れることで、期限付きのものにする。

さっきのコードと足してそれっぽく書くと・・。

const { urls, username, credential, ttl } = await turnService.getCredential({ username: "bob" });
console.log(`TURN credential will be expired in ${ttl}`);

const config = {
  iceServers: [
    { urls, username, credential }
  ]
}

const pc = new RTCPeerConnection({ iceServers });

という感じ。

RESTにすれば、最寄りのTURNのURLを返すこともできるし、TURNサーバー自体のアップデートとかにも耐えられて良いですね。

ICEリスタート

通信経路を決めるICEには、再起動をかける手順があって、それを俗にICEリスタートっていう。

なんかネットワーク(もとい、今使われてる経路)の調子が悪いとか、モバイルからWiFiになったのでつなぎなおしたいとか、そういうときに使う。

ただTURNの場合はここで1つ問題が。
さっきのユーザー名にタイムスタンプが入ってるので、同じクレデンシャルを使い回すと期限切れでおこられが発生する。

そしてTURNにはcredentialを延長するみたいな決まった手順があるわけではなくて、新しくcredentialを発行してねって感じになってる。もちろん任意にできる認証部分をそういうふうに作ればできるけど、疎に作りたい欲が勝つはず。

なので最終的にはこんな感じにする必要がある。

// 再発行(というか単なる新規発行
const { urls, username, credential } = await turnService.getCredential({ username: "bob" });
// 設定のアップデート
webRtcSdk.updateIceServers([{ urls, username, credential }]);
// ICEリスタート
await webRtcSdk.restartIce();

あるべきSDKの実装例でいうとこう。

class WebRtcSdk {
  updateIceServers(iceServers) {
    const config = this._pc.getConfiguration();
    config.iceServers = iceServers;
    this._pc.setConfiguration(config);
  }

  restartIce() {
    const offer = await this._pc.createOffer({ iceRestart: true });
    // O/Aネゴシエーション
  }
}

おまけ: 1

1253706 - Implement RTCPeerConnection.setConfiguration

残念なお知らせ。

Firefoxには`setConfiguration()`が実装されてないため、TURN経由の場合にシームレスにICEリスタートができません!どうしようもない!

現時点ではFirefox 71でも未実装だった。

おまけ: 2

https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-restartice

`createOffer({ iceRestart: true })`ではなく、ズバリ`restartIce()`ってAPIが実装中。

現時点では、Chrome 77以降でのみ使えます。