インターネットからweb dynoにたどり着くためのたった3つのホスト名

インターネットでの情報の取得には、ホスト名が重要な役割を担うことがあります。本稿では、クライアントプログラムからHerokuアプリケーションにHTTPリクエストが届くまでの間にホスト名が活躍する場面を見なおしてみます。

本稿では、https://jp.heroku.com/platform/runtime のようなURLの`jp.heroku.com`の部分をホスト名と呼ぶことにします。

本稿は[Heroku Advent Calendar 2020](https://qiita.com/advent-calendar/2020/heroku)の13日目の記事です。14日目はsilverskyvictoさんによる記事です。


## DNS
クライアントプログラムがHTTPリクエストを送るには、まず、リクエストを送る先のIPアドレスを知る必要があります。通常、Domain Name Service (DNS)がIPアドレスを教えてくれます。本稿の執筆時には、下記のような2つのIPアドレスでした。

```
$ host jp.heroku.com
jp.heroku.com is an alias for obscure-bastion-4611.safe-mesa-5924.herokuspace.com.
obscure-bastion-4611.safe-mesa-5924.herokuspace.com has address 52.198.19.75
obscure-bastion-4611.safe-mesa-5924.herokuspace.com has address 18.182.184.90
```

今回は`52.198.19.75`を使ってみましょう。

## SNI
クライアントプログラムがHTTPSでリクエストを送るためには、サーバに対してTLSの接続を確立する必要があります。ほとんどのHerokuアプリケーションでは、この際にServer Name Indication (SNI)でアクセス先のホスト名を指定する必要があります。

`openssl s_client`プログラムでは`-servername`オプションで指定できます。

```
$ :| openssl s_client -connect 52.198.19.75:443 -servername jp.heroku.com > /dev/null
depth=2 O = Digital Signature Trust Co., CN = DST Root CA X3
verify return:1
depth=1 C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
verify return:1
depth=0 CN = codeish.jp
verify return:1
DONE
```

SNIを指定しなかった場合や、誤ったホスト名を指定した場合には接続を確立できません。(`openssl`のバージョンによってはSNIを指定しない方法が異なります。)

```
$ :| openssl s_client -connect 52.198.19.75:443 > /dev/null
140065721991616:error:14094438:SSL routines:ssl3_read_bytes:tlsv1 alert internal error:../ssl/record/rec_layer_s3.c:1528:SSL alert number 80
$ :| openssl s_client -connect 52.198.19.75:443 -servername www.heroku.com > /dev/null
139966152090048:error:14094438:SSL routines:ssl3_read_bytes:tlsv1 alert internal error:../ssl/record/rec_layer_s3.c:1528:SSL alert number 80
```

## `Host`リクエストヘッダ
無事にHerokuとの接続が確立した場合には、Herokuのルーティング層がリクエストをアプリケーションのweb dynoに転送します。その時に参照するのが、`Host`リクエストヘッダです。

```
$ (printf 'GET /platform/runtime HTTP/1.1\r\nHost: jp.heroku.com\r\n\r\n'; sleep 2) |openssl s_client -connect 52.198.19.75:443 -servername jp.heroku.com -quiet 2>/dev/null | head -25
HTTP/1.1 200 OK
Cache-Control: max-age=0, private, must-revalidate
Content-Language: en
Content-Type: text/html; charset=utf-8
Etag: W/"858279e6f0448bea7ac3228f95e97006"
Referrer-Policy: strict-origin-when-cross-origin
Set-Cookie: 中略; path=/; expires=Wed, 07 Dec 2022 00:48:06 GMT; HttpOnly
Strict-Transport-Security: max-age=31536000
Vary: Accept-Encoding
Via: 1.1 spaces-router (4f676408e4ed)
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Frame-Options: SAMEORIGIN
X-Permitted-Cross-Domain-Policies: none
X-Request-Id: 75f38a39-5703-b3c2-b098-a50fb7e7f6c3
X-Runtime: 0.010791
X-Xss-Protection: 1; mode=block
Date: Mon, 07 Dec 2020 00:48:06 GMT
Transfer-Encoding: chunked

1000
<!DOCTYPE html>
<html lang="ja" data-form-id="">
  <head>
    <meta charset="utf-8">
```

(この例のように`openssl s_client`プログラムを利用するとTLS証明書の検証に失敗しても接続を続けるので注意してください。)

`jp.heroku.com`はPrivate Spaceで稼働しているHerokuアプリで、Hostリクエストヘッダを誤ったもの(ここでは`www.heroku.com`)にすると495 Certificate Errorが返却されます。

```
$ (printf 'GET /platform/runtime HTTP/1.1\r\nHost: www.heroku.com\r\n\r\n'; sleep 2) |openssl s_client -connect 52.198.19.75:443 -servername jp.heroku.com -quiet 2>/dev/null | head -25
HTTP/1.1 495 status code 495
Cache-Control: no-cache, no-store
Content-Type: text/html; charset=utf-8
Via: 1.1 spaces-router (4f676408e4ed)
Date: Mon, 07 Dec 2020 00:50:24 GMT
Content-Length: 562

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta charset="utf-8">
    <title>SSL certificate error</title>
    <style media="screen">
      html,body,iframe {
        margin: 0;
        padding: 0;
      }

      html,body {
        height: 100%;
        overflow: hidden;
      }

      iframe {
```

`nc`コマンドでHostリクエストヘッダの誤ったHTTPリクエスト(HTTPSではなく)を送ると404 Not Foundが返却されます。

```
$ (printf 'GET /platform/runtime HTTP/1.1\r\nHost: www.heroku.com\r\n\r\n'; sleep 2) | nc 52.198.19.75 80 | head -25
HTTP/1.1 404 Not Found
Cache-Control: no-cache, no-store
Content-Type: text/html; charset=utf-8
Via: 1.1 spaces-router (4f676408e4ed)
Date: Mon, 07 Dec 2020 01:16:47 GMT
Content-Length: 549

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta charset="utf-8">
    <title>No such app</title>
    <style media="screen">
      html,body,iframe {
        margin: 0;
        padding: 0;
      }

      html,body {
        height: 100%;
        overflow: hidden;
      }

      iframe {
```

## CDN経由でのHerokuアプリへのアクセス
普段は、ホスト名の取り扱いを気にする必要はありません。DNSさえ期待どおりに設定されていれば、ウェブブラウザ正しく安全に取り扱ってくれます。いっぽう、CDNやWAFを経由してHerokuアプリへアクセスをするよう設定する場合には、TLS証明書の都合やアプリケーションコードでのホスト名の取り扱いの都合から、これら3つのホスト名の設定を工夫する必要があるかもしれません。そんな時には本稿をちらっと思い出してみてください。

流行りのようなタイトルにしてみましたがうまくいきませんした。いかがでしたか？