Minecraft Bedrock クライアントが NetherNet でワールドに到達する方法には、大きく異なる2つの経路があります。公開された Dedicated Server は独自の HTTP シグナリングエンドポイントを実行しており、そこへ直接 offer を POST できます(iceFetch がサーバー向けに使う経路です)。一方、フレンドのワールド(誰かがゲームからフレンドに向けて公開したワールド)には公開エンドポイントがまったくありません。これは Microsoft が運用し、あなたの Xbox 上の関係によって仲介される クラウドシグナリングサービスを通じて到達します。本記事では、このフレンド経路を端から端まで解説します。ワールドがどのように発見され、サービスがどのようにワールドを指定し、ICE candidate がどのように返ってくるのかを見ていきます。
フレンドが公開しているワールドは、Xbox 上で session-directory の activity handle として公開されています。自分のソーシャルグラフに含まれる人々のハンドルを照会します。
POST https://sessiondirectory.xboxlive.com/handles/query?include=relatedInfo,customProperties
{ "type": "activity",
"scid": "4fc10100-5f7a-4470-899b-280835760c07",
"owners": { "people": { "moniker": "people", "monikerXuid": "<your xuid>" } } }
各結果の customProperties.SupportedConnections[0] が、重要な2つの id を保持しています。
"SupportedConnections": [ {
"ConnectionType": 7,
"NetherNetId": 3444785886168534373, // ワールドの WebRTC network id(uint64)
"PmsgId": "1f5d5f53-29ca-46a6-813d-6a4560bc5227" // そのシグナリングアドレス(GUID)
} ]
NetherNetId は 64 ビット整数です。パース済みの数値ではなく 生の JSON テキストから読み取ってください。そうしないと JavaScript が 253 を超える値を丸めてしまいます。このステップは読み取り専用です。セッションへの参加も、リアルタイム(RTA)接続も不要で、通常のユーザー XSTS トークンで十分です。シグナリング WebSocket は MCToken(franchise の「authorization header」)で認可されます。これは通常の Bedrock のチェーンで取得します。Microsoft アカウント → Xbox user token → XSTS → PlayFab LoginWithXbox → franchise session/start の順です。同じアカウントが、offer に必要な a=identity アサーションも発行します(§6 を参照)。
このサービス signal.franchise.minecraft-services.net は、同じホスト上で2つのプロトコルに対応しています。
| レガシー | JSON-RPC | |
|---|---|---|
| パス | /ws/v1.0/signaling/{id} | /ws/v1.0/messaging/connect |
| ピアの指定方法 | 数値の NetherNetId | PmsgId(GUID) |
| 使用箇所 | 古い経路 / LAN 形式の経路 | ライブなゲームクライアントのフレンドワールド |
この区別がすべてを左右します。フレンドワールドの数値の NetherNetId をレガシーエンドポイントに対してダイヤルすると、{ "Code": 1, "Message": "Player not found." } が返ってきます。これは「そのアドレスは登録されていない」という汎用的な応答です。というのも、現在のゲームクライアントのホストは JSON-RPC エンドポイント上に存在し、数値 id ではなく PmsgId で指定されるからです。プロトコルを間違えると、すべてのダイヤルが何のエラーもなく静かに失敗します。
MCToken と2つの相関ヘッダーで接続し、TURN の認証情報を要求してから offer を送信します。宛先は toPlayerId に入れます。内側の message は、それ自体が文字列化された JSON で、自分の network id とシグナルを運びます。
// WebSocket: wss://signal.franchise.minecraft-services.net/ws/v1.0/messaging/connect
// headers: Authorization: MCToken <token> ; session-id: <uuid> ; request-id: <uuid>
// 1) TURN の認証情報
{ "jsonrpc":"2.0", "id":"<uuid>", "method":"Signaling_TurnAuth_v1_0", "params":{} }
// 2) offer、ホストの PmsgId 宛て
{ "jsonrpc":"2.0", "id":"<uuid>", "method":"Signaling_SendClientMessage_v1_0",
"params": {
"toPlayerId": "<host PmsgId>",
"messageId": "<uuid>",
"message": stringify({ // オブジェクトではなく JSON 文字列
"jsonrpc":"2.0", "method":"Signaling_WebRtc_v1_0",
"params": { "netherNetId":"<your id>",
"message":"CONNECTREQUEST <connId> <sdp offer>" } }) } }
応答は Signaling_ReceiveMessage_v1_0 の通知として届きます。それぞれに対して確認応答(ack)を返す必要があります。{ "id": <its id>, "result": null } を返さないと、サーバーは配信をキャンセルします。ネストされた message を展開してシグナルを読み取ります。
CONNECTRESPONSE <connId> <sdp answer> — ホストの SDP answer(その DTLS fingerprint と、すぐに用意できた host candidate)。CANDIDATEADD <connId> <candidate> — trickle された ICE candidate。1メッセージにつき1つです。ホストは、ダイヤル側からいくつか candidate を受け取ってからでないと、自分の candidate を trickle しません。そのため CONNECTRESPONSE が届いたら、CANDIDATEADD メッセージを3つほど送ります。それらしい host candidate でありさえすれば、このしきい値を満たせます。すると、ホストが本物の candidate を返してくるので、それを収集します。
これが、収集される candidate リストが answer SDP よりも通常は長くなる理由です。answer には即座に用意できた host candidate だけが埋め込まれます。一方、srflx(STUN による reflexive なパブリックアドレス)や relay(TURN)の candidate は、収集に往復のやり取りが必要で、後から届きます。
candidate:… udp … 192.168.0.11 56117 typ host
candidate:… udp … 2001:…:e2a5 56117 typ host
candidate:… udp … 58.188.134.137 56117 typ srflx raddr 192.168.0.11 rport 56117
candidate:… udp … 20.202.45.254 54115 typ relay
offer には a=identity 属性が含まれていなければなりません。これは署名付きのアサーション(P-384 鍵に紐づけられた franchise のマルチプレイヤートークン)で、どのアカウントがダイヤルしているかを証明します。これにより、ホストはフレンドを認可し、見知らぬ相手を拒否できます。注目すべきは、candidate を読み取るために、ワールドの Xbox セッションへの参加(MPSD メンバーシップ)も、リアルタイム(RTA)接続の開設も、自分自身の activity の公開も 不要だという点です。有効な identity を伴って、ホストの PmsgId に対して JSON-RPC でダイヤルするだけで十分です。(参考までに、レガシーエンドポイントのエンベロープは数値のタイプコードを使います。0 Error、1 Signal、2 Credentials、3 Accepted、4 Delivered です。)
シグナリングによって、ホストが広告するエンドポイントが得られます。ただし、それ自体がゲーム接続を開くわけではありません。接続を確立するには、完全な WebRTC/DTLS スタックと UDP が必要です。candidate リストにはホストのパブリック IP(srflx の行)が含まれることがあります。これは、ホストが接続を認可したフレンドにすでに提供している情報であり、identity アサーションによってその範囲はあなた自身のアカウントのフレンドに限定されます。とはいえ、まさにそれこそが、この種の機能をオプトインかつ読み取り専用にすべき理由であり、candidate リストを機微な情報として扱う価値がある理由でもあります。
フレンド経路は次のとおりです。Xbox のハンドルを介してワールドの PmsgId を発見し、franchise のシグナリングサービスへ認証し、identity で署名した offer を使って JSON-RPC エンドポイント上でその PmsgId にダイヤルし、いくつか candidate を先に送り、ホストが trickle してくる ICE candidate を収集する、という流れです。最も起こしやすいミスは、数値の NetherNetId をレガシーエンドポイントに対してダイヤルすることです。ライブなワールドは JSON-RPC 上にあり、PmsgId をキーにしています。この一連のフローは、自分のフレンドが公開しているワールドに対して iceFetch → Friends’ worlds で実行できます。また、サーバー側(HTTP)の同等の仕組みは How iceFetch works で確認できます。