Nginxで、リクエストを複製するmirrorモジュールが標準搭載された

Nginxで、リクエストを複製するmirrorモジュールがコミットされ、何もせずとも使用できるようになりそうです(現状最新コミットをビルドする必要あり)。

例えば本番環境のproxyからリクエストを複製して開発環境に流すような事も出来ます。もちろん複製処理は本来のリクエスト処理をブロックしません。

例えば以下のように、mirrorに来たリクエストを複製してバックエンドサーバに投げるようにしてみます
f:id:ASnoKaze:20170724032715p:plain

conf

    server {
        listen       80 ;
        server_name  localhost;

        mirror_request_body on;
        log_subrequest on;

        location /mirror {
            mirror /proxy; #/proxy宛にリクエストを複製する
        }
        location /proxy {
            proxy_pass http://127.0.0.1/proxyed/;#任意のサーバにproxyする
        }
    }
  • mirror ディレクティブでリクエストを複製し、自身の任意のPATHにリクエストを送ります。今回は別のサーバに送るために一旦 /proxy宛に複製します($request_uriは変更されません)。proxy_passではいつも通り任意のサーバにリクエストを送ります。
  • log_subrequestを有効にすると複製したリクエスト自体もaccess.logに記録されるようになります。
  • mirror_request_body はおそらくPOSTのデータと言ったリクエストボディも合わせて複製されるようになるものだと思います。

もちろん2つ以上複製することもできます

動作確認

上記 .conf では、access.logに3つのリクエストが記録される。
本来のリクエスト、log_subrequest onによって記録される複製されたリクエスト、自身にproxyされたのを受け取ったリクエス

vagrant@vagrant:~/nginx-mirror$ curl localhost/mirror
mirror

vagrant@vagrant:~/nginx-mirror$ tail -f /usr/local/nginx/logs/access.log
127.0.0.1 - - [23/Jul/2017:18:08:14 +0000] "GET /proxyed/ HTTP/1.0" 200 0 "-" "curl/7.47.0"
127.0.0.1 - - [23/Jul/2017:18:08:14 +0000] "GET /mirror HTTP/1.1" 200 256 "-" "curl/7.47.0"
127.0.0.1 - - [23/Jul/2017:18:08:14 +0000] "GET /mirror HTTP/1.1" 200 7 "-" "curl/7.47.0"

(本来のリクエストのログは複製処理が終わったあとに出るが、本来のリクエストへのレスポンス処理はブロックされていないように見えました)


便利!!

iphone(iOS 11)でMultiPath TCPを使う

すでにアナウンスされているとおり、iOS11よりMultipathTCP (MPTCP)のAPIサードパーティデベロッパーに公開されます。

実はiPhoneをお使いの人はすでに、iOS7からsiriでMPTCPが使われていたので裏ではすでに使われておりましたが、実際にみんなが試せるような状況になってきました。

MultiPathTCP (MPTCP)とは

MultipathTCPは複数のインターフェースを用いてTCPコネクションをはる事のできる、TCP拡張の一つです。例えばスマートフォンであればLTEWi-Fiといった2つのインターフェースを用いてコネクションをはり、両方の経路それぞれを併用してデータ通信をします。

大きなメリットとしては、使える帯域が増えるということだけではなくアプリケーション側からは1つのTCPコネクションに見えており仮に片方のコネクションが切れたとしてもTCPコネクションは維持できる点です。

Appleの発表では上記ios7のsiriにおいて、95パーセンタイル 20%の高速化と、接続失敗を5分の1に出来たようです。

ハンドシェイクと仕様

MultiPathTCPを使用するためにはクライント・サーバ両方が対応している必要があり、その際はTCP 3ウェイハンドシェク時にオプションとしてMP_CAPABLEが指定されてます。

もちろん既存のTCPコネクション(MP_CAPABLEが有効)に、別経路の通信(subflow)を追加する場合、経路上の機器からは一見ただのTCP 3ウェイハンドシェクが行われたように見えます(MP_JOINオプションが有効になってます)。

詳しくは、RFC6824を読まれるのが良いかと思います(改訂作業中:RFC6824bis)

(もちろん帯域公平性や、悪意あるユーザによるsubflowの追加を防ぐ機能があります (参考:TCP.next // Speaker Deck))

iOS11におけるMPTCP

すでにbeta版で試せるようですが、残念ながら手元に試せる環境がないため、すでに試されている人のコードを拝借させていただきます。
MPTCP experiments on iOS 11 beta — MPTCP

let config = URLSessionConfiguration.ephemeral

config.multipathServiceType = URLSessionConfiguration.MultipathServiceType.handover
let session = URLSession(configuration: config)

let url = URL(string: "http://multipath-tcp.org/data/uml/vmlinux_64")

let task = session.dataTask(with: url!, completionHandler:{...})

task.resume()

URLSessionConfiguration.MultipathServiceTypeは4つのmodeがあり、違いは以下のとおりです

  • none: MultipathTCPを使用しない
  • handover: Wi-Fi回線を優先します。LTEなどのキャリア回線からコネクションを維持したままシームレスにWi-fiにハンドオーバーします
  • interactive: レイテンシを最小にするような経路を使用します。データ量が少ない場合のみ仕様すべきで
  • aggregate: 両方の経路を使用してスループットを向上させます(デベロッパーのみが使用できます)

大まかにデベロッパーがモードを選択できることがわかります。

しかし、ファイアウォールなどによりMP_CAPABLEやMP_JOIN付きハンドシェイクがブロックされてしまったときの挙動や、レイテンシの向きの考慮(参考:One Way Latency Considerations for MPTCPdraft-song-mptcp-owl-01 - One Way Latency Considerations for MPTCP)や、NAT64下での問題(参考pdf)など、実際に試してみないと分からない気はします。


LinuxでのMPTCP対応

実際に試す場合はサーバサイドも準備する必要がありますので、少々古い記事ではありますが参考までに
asnokaze.hatenablog.com

Happy Eyeballs Version 2 の仕様

IPv4IPv6両方が利用可能なデュアルスタック環境において、通信する際にIPv6IPv4を並列的に試してより早く通信の確立が出来た方を試す Happy Eyeballs という仕組みがあります。

より環境の良い方を使うだけでなく、IPv6で通信を試みてからうまくいかない場合にIPv4にフォールバックするのでは無いためより早く通信通信の確立ができます。

Happy EyeballsはRFC 6555で標準化されておりますが、Appleの人によって提案されているHappy Eyeballs Version 2の仕様がIETFで議論されているので簡単に読みます。

Happy Eyeballs Version 2

Happy Eyeballs Version 2は、RFC 6555の頃より何点か変更があります。

まず、大きなところで、名前解決について非同期名前解決の方式が追加されました。今まで名前解決についての規定はありませんでした。AとAAAAを同期的に名前解決する実装もありましたが、早く名前解決出来た方から通信を開始すべき(SHOULD)ということになりました。

従来実装

提案方式

(3月に行われたIETF98の資料(pdf)より)

この非同期式名前解決が実施できない場合は(APIがない場合)、IPアドレスファミリごとに別スレッドを使って同等の事ができる。

その他

  • Happy Eyeballs中に、名前解決の結果が変わった場合の対応について。削除された場合は施行を継続、アドレスが増えた場合は適宜施行リストに追加
  • 以前の通信結果(IPアドレス毎のRTTや、以前TCP Fast Openを使った)などにより、通信施行順番の変更が可能に
  • NAT64 と DNS64を持つIPv6 only環境について

今後

3月に行われたIETF98では、議事録を読む限り興味を持ってる人も多く、またその後にWG Draftに昇格していることからも引き続きホットな議論になるものと思われる。

今月7月に行われるIETF99のv6ops WGのAgendaにもHappy Eyeballs Version 2の議論が行われる予定と鳴っている。

.internal ドメインを予約する提案仕様

IETFのdnsop WGで .internal ドメインを予約する「The .internal TLD.」という仕様が、GoogleのWarren Kumari氏によって提案されています。

DNSに関しては詳しくないのですが、ざっと読んだので簡単にメモ。

.internal

.internalというドメインを内部的に使用しているという方もおられるでしょう。

.internal TLDは、ICANNの「DNS名前衝突ブロックリスト」に含まれており、gTLDになることはありません。その事に関しては以下のINTERNET Watch様の記事が詳しいです。

萌えドメインなのに……「anime.moe」の登録をICANNが禁じている理由とは -INTERNET Watch Watch

しかし、この.internalはIANA(ポート番号や識別子を管理する組織)には登録されていません。現在ドメイン名で特別に予約されているドメインは、.exampleや.localなどがあります。一覧はIANAのWebページから確認できます。

.internal の予約

すでに.internalは使用されており、root DNSへもクエリが漏れてるのだとは思いますが
DNSプロトコルとして .internalを内部利用のために予約する提案が「The .internal TLD.」です。

この提案では、背景、なぜ他の登録済みドメインを利用しないのか、DNSSEC利用時の取扱、それぞれの.internalの扱いについて書かれています。

利用者/オペレータ/レジストラの.internalの扱いは以下のようになるようです

  • アプリケーション及びソフトウェアの作者は .internal を特別に扱う必要はありません
  • キャッシュDNSは .internal を特別扱いしても良いです。ローカルのゾーン情報を使用すべきですが、権威サーバにフォワードしても良いです。
  • 権威サーバは .internal を特別扱いしてはいけません。ローカルでのみ特別な意味を持ちます。
  • DNSオペレータは、.inteenal へのクエリはグローバルなものでないことに注意し、外部にリークされることに注意してください。
  • DNSレジストリ/レジストラは .internal を登録する要求を許可してはいけません。

すでにdnsop WGメーリングリストですでに議論はあったようですが、ドラフトは出たばっかりですのでまだまだ議論が続くことでしょう。

Nginxのリバースプロキシでバックエンドとhttp2通信する

以前書いたとおり、ApacheではリバースプロキシでバックエンドとHTTP2通信することができます。

asnokaze.hatenablog.com

f:id:ASnoKaze:20170531004524p:plain:w450

Nginxの場合は、開発者のメーリングリストGoogleの人が書いてる「ngx_http_v2_upstream」パッチを利用することでバックエンド(upstream)とHTTP2通信することが出来るようになります。

ビルド

環境はUbuntu16.04

$ hg clone http://hg.nginx.org/nginx/
$ cd ./nginx
$ hg update -r 7039

# 順番にパッチを当ててく
$ patch -p1 < ./1.patch
...

$ ./auto/configure  --with-http_ssl_module --with-http_v2_module
$ make

#今回はインストールしてしまう
$ sudo make install

設定

proxy_http_versionに2.0が指定できるようになります。
proxy_passにhttp://を指定すると、h2cダイレクトでバックエンドと接続しに行きます
(証明書は適当に準備)

#nginx.conf
    proxy_http_version 2.0;

    upstream backend {
        server 127.0.0.1;
    }

    server {
        listen       443 ssl http2;
        server_name  localhost;

        ssl_certificate      /home/yuki/server.crt;
        ssl_certificate_key  /home/yuki/server.key;

        location / {
          proxy_pass http://backend/test;
        }
    }

その他、h2cダイレクトではなくhttpsでプロキシする際に、ALPNを使うように出来るようですがちょっと動作が怪しいようです....

proxy_ssl_alpn on;

動作確認

バックエンドはh2cで接続できるようにしておきます。

#nginx.conf
    server {
        listen       80 http2;
        server_name  localhost;
    }

ブラウザでサーバにアクセスすると、バックエンド(/proxy/)もHTTP/2.0で接続できている事が確認できました

$ tail /var/log/nginx/access.log
127.0.0.1 - - [02/Jul/2017:16:36:40 +0000] "GET /proxy/ HTTP/2.0" 404 571 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"
192.168.0.104 - - [02/Jul/2017:16:36:40 +0000] "GET / HTTP/2.0" 404 571 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"

Webページを丸ごとパッケージングする Web Packagingとは

Webページを丸ごとパッケージングする、Web Packagingの仕様がIETFで提案されています

オフラインやローカル環境で共有出来るようになっており、主な特徴は

  • 証明書及び署名がつけられるため、そのパッケージの真正性が確認できる
  • HTTPリクエスト/HTTPレスポンスも含まれている(HPACKを利用)
  • サブパッケージが利用でき、複数のオリジンをパッケージング可能
  • データはCBORで表現される (RFC 7049 - Concise Binary Object Representation (CBOR))
  • Index化されており、オフセットを用いて各リソースにランダムアクセス可能

以上の点で、ZIPで固めて保存するのとはまったく違うことがわかるかと思います。

ユースケース

幾つかユースケースがありますが、主なものは以下のとおりです

  • ローカル環境での共有。SDカードでの共有から、Physical Webと言ったその場でのBluetoothを用いた配信など
  • Webのスナップショット保存。そのページをスナップショットとして保存する(署名無し)
  • CDNでの配布。CDNや別の場所から再配布することが可能。この場合は署名をつけての配布が可能となる

提案仕様と経緯

6月30日に、GoogleのJeffrey Yasskin氏より「Web Packaging」という提案がIETFにて出されました。おそらく議論はDispatch WGで行われ、今月実施されるIETF99
IETF 99
でもオフラインでの議論があるかもしれません。

もともと、Webページをパッケージングする仕様は、W3Ctag「Packaging on the Web」というものがありましたが、そこから署名などの機能を追加し整理した「Web Packaging」という仕様がW3CのWICGで議論されておりました。

このW3C WICGの仕様は、データフォマットが要でありIETFで標準化していくのが適切ではないかとIETFに持ち込まれた形になります。

フォーマット

webpackageのフォーマットは以下の通りである

webpackage = [
  magic1: h'F0 9F 8C 90 F0 9F 93 A6',  ; 🌐📦 in UTF-8.
  section-offsets: { * (($section-name .within tstr) => offset) },
  sections: ({ * $$section }) .within ({ * $section-name => any }),
  length: uint,                        ; Total number of bytes in the package.
  magic2: h'F0 9F 8C 90 F0 9F 93 A6',  ; 🌐📦 in UTF-8.
]
  • magic: 先頭と最後に挿入されるMagicコード。意味はない
  • section-offsets: 各セクションへのオフセット値が与えられる。必須ではない
  • sections: ここにManifestや実際のコンテンツが格納される(manifest, indexed-content)
  • length: データ長

manifest及び、indexed-contentは以下で説明します。

manifest

sectionsに格納されるmanifestデータ。必須ではない。

日付、オリジン名、証明書、署名が格納される。また、アレば別オリジンのパッケージをサブパッケージとして読み込むことが出来る。

具体的には以下のパラメータです

  • metadata: 日付、及びオリジンが記述される。また、あればサブパッケージの指定
  • resource-hashes: 各リソースのハッシュ値
  • signatures: 署名
  • certificates: 証明書

    "manifest": {
      "manifest": {
        "metadata": {
          "date": 1(1494583200),
          "origin": 32("https://example.com")
        },
        "resource-hashes": {
          "sha384": [
            h'3C3A03F7C3FC99494F6AAA25C3D11DA3C0D7097ABBF5A9476FB64741A769984E8B6801E71BB085E25D7134287B99BAAB',
            ...
          ]
        }
      },
      "signatures": [
        {
          "keyIndex": 0,
          "signature": h'3044022015B1C8D46E4C6588F73D9D894D05377F382C4BC56E7CDE41ACEC1D81BF1EBF7E02204B812DACD001E0FD4AF968CF28EC6152299483D6D14D5DBE23FC1284ABB7A359'
        }
      ],
      "certificates": [
        DER(
          Certificate:
              ...
              Signature Algorithm: ecdsa-with-SHA256
                  Issuer: C=US, O=Honest Achmed's, CN=Honest Achmed's Test Intermediate CA
                      Public Key Algorithm: id-ecPublicKey
                          Public-Key: (256 bit)
                          pub:
                              ...
        ),
        DER(
          Certificate:
              ...
        )
      ]
    },
indexed-content

sectionsに格納される、indexed-content。ここにコンテンツの中身が格納される。
各リソースに対するHTTPリクエスト及びHTTPレスポンスが合わせて格納されており、ヘッダに関してはそれぞれHPACKでエンコードされている。

    "indexed-content": [
      [
        [ hpack({
            :method: GET
            :scheme: https
            :authority: example.com
            :path: /index.html
          }), 1]
        [ hpack({
            :method: GET
            :scheme: https
            :authority: example.com
            :path: /otherPage.html
          }), 121],
        [ hpack({
            :method: GET
            :scheme: https
            :authority: example.com
            :path: /images/world.png
          }), 243]
        ],
      ],
      [
        [ hpack({
            :status: 200
            content-type: text/html
            date: Wed, 15 Nov 2016 06:25:24 GMT
            expires: Thu, 01 Jan 2017 16:00:00 GMT
          }),
          '<body>\n  <a href=\"otherPage.html\">Other page</a>\n</body>\n'
        ]
        [ hpack({
            :status: 200
            content-type: text/html
            date: Wed, 15 Nov 2016 06:25:24 GMT
            expires: Thu, 01 Jan 2017 16:00:00 GMT
          }),
          '<body>\n  Hello World! <img src=\"images/world.png\">\n</body>\n'
        ],
        [ hpack({
            :status: 200
            content-type: image/png
            date: Wed, 15 Nov 2016 06:25:24 GMT
            expires: Thu, 01 Jan 2017 16:00:00 GMT
          }),
          '... binary png image ...'
        ]
      ]
    ]

セキュリティについて

オフラインの利用を想定しているため、証明書の失効確認については難しいところがあります。Webパッケージの利用者は正当性を確認する際は、オンライン時はOCSPを利用して失効確認が可能ですが、オフラインのときはブラウザによって判断されることになるでしょう(1週間だけ有効など)。

そこの部分については今後の議論になるかと思われます。

TLS1.3の0-RTT通信と、HTTP 4NN(未定) ステータスコードの提案仕様

TLS1.3では、0-RTTハンドシェイクの際にearly_dataとしてアプリケーションデータを送信できます。しかし、この0-RTTハンドシェイクで送信するデータには、リプレイ攻撃のリスクがあります。
そのリスクをさけるための機能をHTTPに追加する提案が出ています。

提案仕様は、「Using TLS Early Data in HTTP」で公開されており、著者はMartin Thomson, Mark Nottinghamらである。

この仕様ではHTTPに以下の機能を追加します

背景

現在標準化の大詰めを迎えているTLS1.3の機能の一つに 0-RTT Data というものがります。

これは、一度TLSハンドシェイクを行ったサーバに対して再度ハンドシェイクを行う際は、ClientHelloと共にアプリケーションデータも送信する手順です。通常のフルハンドシェイク手順よりも早くアプリケーションデータを送信し始めることができます。パケットの往復を待たずに送りたいデータを送ることから、0-RTTハンドシェイクとも呼ばれます。

具体的な手順は、TLS1.3の仕様の2.3節で示されています
f:id:ASnoKaze:20170618234412p:plain:w350

この0-RTTハンドシェイクで送信されるアプリケーションデータはもちろん暗号化されていますが、リプレイ攻撃が可能です。0-RTTハンドシェイクのClientHelloのパケットを観測し、再度サーバに送りつけ多重にリクエストを処理させることができます。

そのため、この0-RTTハンドシェイクで送信するアプリケーションデータはべき等なもの、つまり、なんど処理をしても結果が変わらないものに限られるべきです。HTTPで言えばGETメソッドなどがそう言えるでしょう(そうでないものも多いですが)。

4NN (Too Early)と、Early-Dataヘッダ

この 0-RTT Dataのリスクを軽減するために「Using TLS Early Data in HTTP」という提案がでています。この仕様ではHTTPに以下の機能を追加します

4NN (Too Early)

4NN (Too Early)は、サーバがリプレイ攻撃の危険性がある0-RTTハンドシェイクのearly_dataデータを処理したくないことをクライアントに通知するステータスコードです。

このステータスコードを受け取ったクライアント(User-Agentやプロキシ)は自動でリクエストを再試行すべきではありません、全く新しいコネクションとしてサーバに接続する必要があります。

418 VS その他

具体的なステータス番号については、Author間で議論がありました。まだIANAに登録されていない418を使うという提案がありましたが、Hyper Text Coffee Pot Control Protocolで「418 I'm a teapot」として使用されています。

「418 I'm a teapot」は https://www.google.com/teapot 実際にデプロイされておりますが、HTCPCPはジョークRFCであり、HTTPとは別のプロトコルであるためステタース番号はかぶっても良いという意見です。

また、ジョークRFCによって貴重な番号が消費される事についても意見がついているようです。

しかし最終的には関係者と議論して決めるようです。

Early-Dataヘッダ

TLSを終端するサーバとアプリケーションサーバが別れている構成は一般的です。もちろんCDNやクラウドサービスのTLS終端サービスはそのHTTPリクエストがリプレイ攻撃されても問題ないか判断はできません。

そのため、TLS終端したサーバはそれが元々0-RTTハンドシェイクで送られてきたデータなのか区別できるようにする必要があります。

仕様では、HTTPリクエストが0-RTTearly_dataで送信されていることを示すEarly-Dataヘッダが定義されています。0もしくは1であり、1がearly_dataで送信されていることを示します。

TLSを終端しているサーバがEarly-Dataヘッダを付与します。"Early-Data"ヘッダーフィールドは、ユーザーエージェント(つまり最初のリクエストを送った者)が使用するためのものではありません。

f:id:ASnoKaze:20170619000602p:plain

この時、4NNを受け取った場合の終端サーバはクライアントにそれを転送しなければなりません。

またEarly-Dataヘッダは、4NNステータスコードを理解できることも示しています。