DIALというネットワークプロトコル
mzyy94
mzyy94
17 min read

Categories

Tags

家庭のネットワークの監視システムからDIALなるプロトコルが暴れていてアラートが飛んできたので調査しました。

Table of Contents

  1. 日頃の監視と増えた謎のリクエスト
  2. オープンDIALプロトコル
    1. DIALについて
  3. DIALを喋る相手を突き止める
  4. DIALでNetflixを操作する
    1. DIALでNetflixの状態を確認する
    2. DIALでNetflixを起動してみる
    3. DIALでNetflixを非表示にする
    4. DIALでNetflixを終了する
  5. 他のアプリも操作してみる
  6. DIAL対応アプリと挙動
    1. ChromeのDIALによる起動API
    2. DIALによる起動後の操作連携
  7. まとめ

日頃の監視と増えた謎のリクエスト

自宅のシステムを管理する上で、ネットワーク監視は重要なものと言えます。 そんな監視システムが異常を検知したと言うのでパケットを見てみると、ある日を境に自宅ネットワークに何やら見慣れないものが大量に流れ始めていました。

Network Monitoring

M-SEARCH * HTTP/1.1で始まるUDPパケットから見慣れたUPnP(Universal Plug and Play)の探索リクエストというのはわかるのですが、これが今まで以上に飛び交うようになっていたのです。 リクエストの中身には、urn:dial-multiscreen-org:service:dial:1というリソース名(URN)を持っていることがわかります。

Universal Plug and Play - Wikipedia

オープンDIALプロトコル

このURNが示すものとは一体何なのか、日本語での解説は唯一、Amazonの開発者向けサイトに記載がありました。

DIALについて

Amazon Fire TVデバイスは、Whisperplayサービスを介してDIAL(Discovery-and-Launch)プロトコルをサポートします。DIALは、別のデバイスからセカンドスクリーンアプリを使用してFire TV対応アプリを検出し起動できるようにするオープンプロトコルです。そのためには、Fire TVとセカンドスクリーンデバイスが同じネットワークに存在する必要があります。

DIALは、キャスティングやミラーリングの機能を提供するAPIではありません。セカンドスクリーンデバイスのアプリがFire TVでアプリを見つけて起動できるようにするだけです。通常は、セカンドスクリーンアプリ(起動メッセージの送信側)と、対応するファーストスクリーンアプリであるFire TV対応アプリ(メッセージの受信側)の両方を実装します。

引用元: DIALの統合 | Amazon Fire TV

なるほど、Fire TVなどのスマートテレビデバイスにあるアプリケーションを検出して起動するためのオープンなプロトコルであるとのことです。 DIAL公式サイトでプロトコル仕様のPDFが提供されていました。

DIAL —DIscovery And Launch— | www.dial-multiscreen.org

DIALを喋る相手を突き止める

しかしながら、我が家にはFire TVはありません。一体どのデバイスがこのDIALを喋るようになってしまったのでしょうか。 仕様書を読みながら簡単に検出スクリプトを書いて実行してみます。

これを実行すると、標準出力には次のような内容が出力されました。

HTTP/1.1 200 OK
LOCATION: http://192.168.181.222:56790/dd.xml
CACHE-CONTROL: max-age=1800
EXT:
BOOTID.UPNP.ORG: 1
SERVER: Linux/2.6 UPnP/1.0 quick_ssdp/1.0
ST: urn:dial-multiscreen-org:service:dial:1
USN: uuid:82152303-4d0c-4cba-92e8-9614ee8aff70::urn:dial-multiscreen-org:service:dial:1
WAKEUP: MAC=96:14:ee:8a:ff:70;Timeout=120

仕様書と照らし合わせて読むと、HTTPプロトコルで http://192.168.181.222:56790/dd.xml に DIALの Device Description があるとわかります。

このURLを開いてみると、次のようになっていました。

Device Description

DIALを喋っている子は先日買い替えた75インチテレビのHISENSE-75A6Gでした! 振り返ってみると、ちょうどテレビを買い替えた日からUPnPのマルチキャストパケットが大量発生していました! インターネットテレビに対応したスマートTVを導入したのが今回が初めてなので、それによって急増したマルチキャストパケットが異常としてアラートに引っかかってしまったのです。

インターネットテレビ - Wikipedia

Amazon | ハイセンス 75V型 4Kチューナー内蔵 液晶 テレビ 75A6G ネット動画対応 ADSパネル 3年保証 2021年モデル | テレビ 通販

DIALでNetflixを操作する

DIALプロトコルはNetflixが策定しています。そしてHISENSE-75A6GにはNetflixアプリが搭載されています。 このNetflixがリファレンス実装をしていると想定し、仕様書に書かれている手順を用いてDIALでNetflixを起動してみるとします。

DIALでは先ほどのDevice DescriptionのレスポンスヘッダーにあったApplication-URL:をベースとして、末尾にアプリケーション名を追加したエンドポイントをApplication Resource URLと呼び、そこに対してリクエストを行います。 ここで用いるアプリケーション名は、DIAL Registryに登録されていると仕様書に書かれています。

Application Name/Prefix Registry - DIAL

早速Netflixのアプリケーション名をDIAL Registryで探したところ、Netflixでした。 このNetflixとApplication-URLのhttp://192.168.181.222:56789/apps/を合わせたhttp://192.168.181.222:56789/apps/Netflixがエンドポイントとなります。

このエンドポイントを例にとると、Netflixアプリに対して次のような操作が可能となっています。

  • GET /apps/Netflix: Netflixの情報の取得
  • POST /apps/Netflix: Netflixの起動
  • POST /apps/Netflix/run/hide: Netflixの非表示
  • DELETE /apps/Netflix/run: Netflixの終了

これを踏まえて、DIALでアプリケーションを起動するためのフローは次のようになっています。

DIAL REST Service: Application Launch DIAL REST Service DIAL REST Service DIAL Client DIAL Client opt Find out if Netflix app exists (1) GET <Application-URL>Netflix (2) 200 OK Launch Netflix app (3) POST <Application-URL>Netflix (optional argument) Launch Netflix app w/optional argument (4) 201 CREATED w/LOCATION header

DIALでNetflixの状態を確認する

まず状態を確認するため、GETリクエストを送ってみます。

$ curl -s 'http://192.168.181.222:56789/apps/Netflix'
<?xml version="1.0" encoding="UTF-8"?>
<service xmlns="urn:dial-multiscreen-org:schemas:dial" dialVer="2.2">
  <name>Netflix</name>
  <options allowStop="true"/>
  <state>stopped</state>
  <additionalData>

  </additionalData>
</service>

おお、ちゃんとそれらしきレスポンスが返っててきました。

DIALでNetflixを起動してみる

同じエンドポイントに対してPOSTリクエストを送ることで、テレビにはNetflixの画面が映し出され、起動することができました。

$ curl -XPOST -s -D - 'http://192.168.181.222:56789/apps/Netflix'
HTTP/1.1 201 Created
Content-Type: text/plain
Location: http://192.168.181.222:56789/apps/Netflix/run
Access-Control-Allow-Origin: (null)

レスポンスヘッダーに含まれるLocationが示すURLが、現在起動中のインスタンスに対するREST操作を受け付けるエンドポイントとなります。

DIALでNetflixを非表示にする

インスタンスのエンドポイントに/hideを加えたURLに対してPOSTリクエストを行うと、現在起動中のDIALアプリケーションを非表示にしてバックグラウンド動作に切り替えられます。

$ curl -XPOST -s -D - 'http://192.168.181.222:56789/apps/Netflix/run/hide'
HTTP/1.1 503 Service Unavailable
Content-Type: text/plain
Content-Length: 50
Connection: close

Error 503: Service Unavailable
Service Unavailable

なぜか503エラーが返ってきますが、テレビで起動しているNetflixは非表示になり、起動する前の画面に戻りました。

DIALでNetflixを終了する

インスタンスのエンドポイントに対してDELETEリクエストを行うと、現在起動中のDIALアプリケーションを終了できます。

$ curl -XDELETE -D - 'http://192.168.181.222:56789/apps/Netflix/run'
HTTP/1.1 200 OK
Content-Type: text/plain
Access-Control-Allow-Origin: (null)

テレビで起動しているNetflixは終了し、成功レスポンスが返ってきました。

他のアプリも操作してみる

HISENSE-75A6GにはいくつかVODサービスが登録されているので、DIAL Registryに登録されているものを探してみました。

VODサービス名 DIAL対応 DIALアプリケーション名
Netflix Netflix
YouTube YouTube
Prime Video AmazonInstantVideo
Hulu × n/a
ABEMA × n/a
U-NEXT × n/a
dTV × n/a
Paravi × n/a
スカパー × n/a
TSUTAYA TV × n/a
DMM.com × n/a
Rakuten TV × n/a

DIAL RegistryにはHuluの名前はあるものの、日本のHuluは本家Huluとは別物の”名ばかりHulu”1なので、DIALには対応していないようです。

Netflixの例では特にリクエストヘッダーをつけなくてもcurlでDIALリクエストを送ることができていました。 しかしYouTubeは、Netflixの例で示したようなシンプルなリクエストでは403エラーで弾かれてしまいます。 これを解決するにはDIALプロトコル仕様の6.6 CORS Requirements and CORS Access Control Policyを読み込み、開発者の意図を汲み取る必要があります。

6.6には次のように記されています。

Whenever an HTTP request is made against an Application Resource, the DIAL server should run the following checks:

  1. If the ORIGIN header is absent in the request, the CORS check is not applicable and the request is allowed.
  2. If the ORIGIN header is present in the request:
    1. The ORIGIN header may indicate the https scheme. The full hostname of the ORIGIN header must match one of the domains authorized for the https scheme. Alternatively, all single-level subdomains within a specific authorized domain may be accepted if explicitly authorized by a DIAL application. The set of authorized domains is specific to each DIAL application.
    2. Additional ORIGIN header schemes that are considered secure resources may also be accepted, such as the package scheme.

リクエストにOriginヘッダーなければリクエストが許可され、Originヘッダーがある場合は、そのドメインでCORS判定を行うよう書かれています。 この仕様に従うと、Netflixのようにリクエストヘッダーをつけなくても、すなわちOriginヘッダーが無くてもリクエストは許可されるはずですが、YouTubeは403エラーが出ます。

ここからが肝心なところなのですが、この節はshouldで書かれています。 開発者の意図を汲み取り、きっとYouTubeの開発者はOriginヘッダーが無い場合にリクエストを許可する実装をあえてしなかったと推測する必要があります。 それができればあとは簡単で、Origin: https://www.youtube.comをリクエストヘッダーにつけてあげればNetflixと同様のDIAL操作ができるようになります。

挙動を調べてみたところ、今回見つかったDIALアプリが許容するOriginヘッダーの値は次の通りでした。後に判明したことですが、後述するホワイトリストにいくつかのアプリの許容オリジンが書かれています。

  • Netflix: https://www.netflix.com, package:スキーマで始まる任意のURI
  • YouTube: https://www.youtube.com, package:スキーマで始まる任意のURI
  • Netflix: https://www.amazon.com, package:スキーマで始まる任意のURI

DIAL対応アプリと挙動

冒頭で紹介したAmazon開発者サイトの説明にあった通り、DIALは対応アプリの探索と起動をするプロトコルで、キャスティングやミラーリングの機能を提供するAPIではありません。 仕様を見ても挙動を確認しても、探索と起動しかできないことがわかりました。

そんなDIALの使いどころとは、いったいどこにあるんでしょうか。

単純な起動をするだけのこのプロトコルはどういった目的で使われるのか、その答えも単純で、スマホアプリなどが同じネットワークに存在する対応アプリを探索し起動までをするためです。 起動までできれば、あとはアプリの範囲内で自由にネットワーク通信を確立できるので、相互にTCP/IPで接続しあったりサービスのAPIを用いてクラウドを経由したりと思い思いの手段を用い、動画の再生をコントロールするようになります。

手持ちのデバイスでは、NetflixのiOSアプリとYouTubeのiOSアプリ、そしてyoutube.comにアクセスしているChromeやVivaldiなどのChromiumベースのブラウザが、このDIALによってDIAL対応アプリを探索し起動していました。

YouTube DIAL connection menu on Vivaldi

YouTube DIAL connection menu on iOS Netflix DIAL connection menu on iOS

ChromeのDIALによる起動API

youtube.comにアクセスしているChromeでDIAL操作ができるということは、JavaScriptでDIALによって対応アプリの起動ができるということを意味しています。 調べてみると、Chromiumの実装は以下の箇所にDIAL関連のソースコードがありました。

media/router/discovery/dial/chromium/README.md at 94.0.4595.1 · chromium/chromium

このmedia/router下の階層のソースコードはChrome Media Routerというコンポーネントの実装で、Chromecastなどのデバイスとコンテンツのコントロールを行うブラウザAPIを提供しています。 この低いレイヤーのAPIを拡張機能なしでJavaScriptから扱うために、Cast Chrome Sender SDKが使えるとあります。

Web Sender API  |  Cast  |  Google Developers

早速Cast SDKリファレンスとインテグレーションガイドを読み、SDKの中身を紐解きながらDIALアプリの起動コードを実装してみました。DIALで起動させる肝となるのは、Cast Framework APIではなくBase APIを使い、DialRequestを正しくセットできるかという部分でした。

const sessionRequest = new chrome.cast.SessionRequest(
  chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID
);
sessionRequest.dialRequest = new chrome.cast.DialRequest(dialAppName);

数十行のコードでブラウザから簡単に起動ができるコードが完成したかと思いきや、うまく動きません。実装は絶対に間違っていないのに。。

そこでさらに探っていくと、NetflixやYouTubeはホワイトリスト形式でドメイン判定があり、特定のオリジンでしかDIALによる起動ができないようになっていました。 ただ、Amazon Prime VideoはChrome 94相当のソースコードには判定コードが含まれていなかったので、Amazon Prime Videoを起動するサンプルを以下に用意しました。 対応デバイスが同一ネットワークにあれば、Chrome 94前後でAmazon Prime VideoのDIALアプリの起動を試すことができるはずです。

Waiting to connect.

Source Code

Demo preview

DIALによる起動後の操作連携

YouTubeは起動後、こちらはQUICでYouTubeのAPIサーバーと通信して操作があるたびにクラウドを経由して再生コントロールをする形になっていました。

Netflixはというと、TCP 9080ポートでHTTPサーバーをiOSアプリとテレビの両方で立ち上げ、立ち上がっていることをUPnPで確認しあい、次に示すフローで暗号化されたセッション情報を相互に送り合いながら動画の再生コントロールを行っていました。仕組みとしてはMDX(Multiple-Device Experience)2を使っていて、CTicketのペイロードにはCBOR3が使われるなど、なかなか面白い作りになっていました。

2nd Screen Control Session Controller Target POST /mdxping action=pingsearch 200 OK status=ok POST /mdxping action=pingresponse 200 OK status=ok encrypted [ AES-128-CBC w/ HMAC-SHA256 hash ] Pairing POST /pairingrequest Send CTicket (CBOR) 200 OK status=ok POST /pairingresponse Send encrypted shared secret 200 OK status=ok Session Play an episode POST /session 200 OK status=ok Report playing state POST /session 200 OK status=ok

まとめ

異常なほどにUDPマルチキャストが飛び交っていたのは、テレビをHISENSE-75A6Gに買い替えたことによって、VivaldiがHISENSE-75A6Gを見つけてしまったことに起因していました。 ご家庭のネットワークで異常検知アラートがなったら調査する習慣をつけておくと、こういった問題の解決に迅速に対応できるのでアラート設定をお勧めします。

DIALの挙動を一通り確認することもでき、また新たな技術を知る良いきっかけとなりましたとさ。おしまい。