0.0.0.0にはアクセスしないこと

# はじめに

この記事は2019年4月時点で調べたものをベースにしています。将来的に変わるかもしれません。

## tl;dr

- `0.0.0.0`を宛先に使うのは誤り
- ただしOSによっては `127.0.0.1` に到達するので支障がなかったりする

## 想定読者

`0.0.0.0`と`127.0.0.1`の違いをすぐに答えられない人が対象です。
ネットワークな人はわかっていることだと思うのでブラウザバックしてもらって構いません。
強い人は間違えているところコメントください。

## 環境

Ubuntu 16.04を利用します。

```console
$ uname -a
Linux parallels-vm 4.10.0-28-generic #32~16.04.2-Ubuntu SMP Thu Jul 20 10:19:48 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
```

## 簡単な例

Webの文脈ではWebサーバのQuick Startによく登場する気がします。

例えば、pythonのhttpモジュールで簡単にHTTPサーバが建てられますが、ここで`0.0.0.0`が見られます。

```console
$ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
```

上記のように表示されたら、何も考えずにブラウザで`http://0.0.0.0:8000/`を開いて（シェルの`http://0.0.0.0:8000/` の文字列にカーソルを合わせてCtrl + クリック）、用意したコンテンツを確認。

<img width="596" alt="スクリーンショット 2019-04-02 午後11.46.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/266939/a3325c0c-3808-b232-9ebc-d11c0820e985.png">

hello world!と表示されましたね。

![localhost.png](https://qiita-image-store.s3.amazonaws.com/0/266939/f4ab5893-4988-9c14-52e6-ce7232498218.png)

今回の挙動は図のような動きになります。
1. pythonのhttpサーバがLISTENしている
2. 同一マシン上からブラウザで`http://0.0.0.0:8000/`にHTTP GETリクエストを飛ばします
3. リクエストを受けたpythonは`index.html`ファイルをステータス200で返します
4. ブラウザはHTTP Responseを画面に描画します

```html:index.html(htmlじゃないけど許して)
hello world!
```


# 0.0.0.0

先程は`http://0.0.0.0:8000/`で待ち受けているサーバにブラウザでアクセスして、サーバが`index.html`ファイルを返しました。
でもちょっと違和感がありますよね。 `0.0.0.0`って何でしょうか? 結果的には同じマシン上のサーバにアクセスできているようですが、自身を表すアドレスは `0.0.0.0`ではなく `127.0.0.1` のはず。

(追記:2019/04/07)
`127.0.0.1`は自身を表すアドレスではないという[コメント](http://b.hatena.ne.jp/entry/4666866507095334370/comment/ghostbass)を見て、言われてみれば確かにそうなので追記しておきます。

> A datagram sent by a higher-level protocol to an address anywhere within this block loops back inside the host. 

obsoletedですが[RFC5735](https://tools.ietf.org/html/rfc5735#section-3)の記述でこのように書かれているので、`127.0.0.1/8`は名前の通り結果的に自身に戻ってくるアドレスで自身を表すアドレスではないという意図なのかなと思います。


(追記ここまで)

## Wikipedia

ひとまずwikipediaを読んで概要を掴みましょう。

> 無効、不明、または適用外の対象を指定するために使用されるルーティング不可のメタアドレスである。

> ホストアドレスとしての0.0.0.0の用法には、以下のものがある。

> - 「任意のIPv4アドレス」を意味する。サーバを設定するとき（すなわちlistenするソケットをバインドするとき）に使用される。C言語ではINADDR_ANYとしてマクロ定義されている（bind(2)はインタフェースではなくアドレスにバインドする）。
> - ホストにまだアドレスが割り当てられていないときに、ホストが自分自身を指すのに使用するアドレス。DHCPで最初のDHCPDISCOVERパケットを送信するときなどに使用する。
> - DHCPによるアドレス取得に失敗したときに、ホストが自分自身に割り当てるアドレス（ホストのIPスタックが対応している場合）。最近のオペレーティングシステムでは、これはAPIPAメカニズムに置き換えられている。
> - 対象が利用できないことを明示的に指定する[1]。
>
> サーバにおいては、0.0.0.0は「ローカルマシン上の全てのIPv4アドレス」を意味する。ホストに192.168.1.1と10.1.2.1の2つのIPアドレスがあり、そのホストで実行されているサーバが0.0.0.0で待ち受けするように構成されている場合、どちらのIPアドレスに対しても到達可能になる。

https://en.wikipedia.org/wiki/0.0.0.0
https://ja.wikipedia.org/wiki/0.0.0.0

Wikipediaによると今回のpythonのHTTPサーバが表示した `Serving HTTP on 0.0.0.0`の意味はローカルマシン上の全てのIPv4アドレスとのことです。
例えば次のようにホストがIPv4インターフェースを2つ持っているときに `0.0.0.0` でサーバを起動するとどちらのインターフェースでも到達可能になります。

```console
$ python -m http.server & # backgroundでサーバ起動
Serving HTTP on 0.0.0.0 port 8000 ...
$ netstat -antu | grep '0.0.0.0:8000'
tcp        0      0 0.0.0.0:8000            0.0.0.0:*               LISTEN   

$ ip address show | grep 'inet ' # hostのinterfaceを確認
    inet 127.0.0.1/8 scope host lo
    inet 10.211.55.6/24 brd 10.211.55.255 scope global dynamic enp0s5
$ curl 'http://127.0.0.1:8000/' # １つ目のインターフェースの応答確認
127.0.0.1 - - [03/Apr/2019 00:53:26] "GET / HTTP/1.1" 200 -
hello world!
$ curl 'http://10.211.55.6:8000/' # 2つ目のインターフェースの応答確認
10.211.55.6 - - [03/Apr/2019 00:53:53] "GET / HTTP/1.1" 200 -
hello world!
```

また、`127.0.0.1`との違いという観点では、`0.0.0.0`で起動した場合は同じネットワークの他のホストからも`http://10.211.55.6:8000`で到達可能ですが、サーバを`127.0.0.1`で起動した場合は他のホストからは到達不可能という違いが生じます。
蛇足ですがlocalhostはたいてい`127.0.0.1`に名前解決されるよう`/etc/hosts`に記述されています。なので`http://127.0.0.1:8000/`は`http://localhost:8000/`とすることもできます。

```console:/etc/hosts
$ cat /etc/hosts | grep '127.0.0.1'
127.0.0.1	localhost
```


これで、サーバが`0.0.0.0`でLISTENする意味と`127.0.0.1`との違いがわかりました。ここまでは教科書通り。
一方、宛先に`0.0.0.0`を指定した場合はどうなるのでしょうか。 `0.0.0.0`というアドレスにルーティングされるのでしょうか? でもそんなアドレス存在しないし、よくわかりません。


## RFC

それではより正確な情報を得るため、RFCを見てみます。
RFC6890では、Special IPv4 Addressとして`0.0.0.0/8`を規定しています。
これによると、IPv4の`0.0.0.0/8`(先頭8bitが0)のアドレスは「このネットワーク上のこのホスト」という意味のようです。

>   Tables 1 though 16, below, represent entries with which IANA has
>   initially populated the IPv4 Special-Purpose Address Registry.
>
>              +----------------------+----------------------------+
>              | Attribute            | Value                      |
>              +----------------------+----------------------------+
>              | Address Block        | 0.0.0.0/8                  |
>              | Name                 | "This host on this network"|
>              | RFC                  | [RFC1122], Section 3.2.1.3 |
>              | Allocation Date      | September 1981             |
>              | Termination Date     | N/A                        |
>              | Source               | True                       |
>              | Destination          | False                      |
>              | Forwardable          | False                      |
>              | Global               | False                      |
>              | Reserved-by-Protocol | True                       |
>              +----------------------+----------------------------+
>
>                    Table 1: "This host on this network"

https://tools.ietf.org/html/rfc6890#section-2.2.2

ここで注目したいのが、`Destination = False` です。つまり、`0.0.0.0/8`のアドレスは宛先として使用しないアドレス帯(**無効な宛先アドレス**)として定義されているということです。

それでは最初の例でブラウザに`0.0.0.0:8000`を入力して自身のサーバに到達したのは何だったのでしょうか? `/etc/hosts`のように、誰かが`0.0.0.0`をloopback addressに変換しているのでしょうか?

# Google Chrome

Google Chromeが気を聞かせて`0.0.0.0`を入力した際に`127.0.0.1`にアクセスしているのでしょうか?
色々と賢い処理を裏で動かしているGoogleならやりかねません。

これを確かめる方法は、ブラウザを使わずに `0.0.0.0`にアクセスすると良さそうです。

```console
$ python3 -m http.server &
Serving HTTP on 0.0.0.0 port 8000 ...
$ curl http://0.0.0.0:8000/ # 宛先0.0.0.0にアクセス
127.0.0.1 - - [03/Apr/2019 01:26:27] "GET / HTTP/1.1" 200 -
hello world!
```

正常にサーバが応答を返してきました。Google Chromeの仕業ではなさそうです。(もしかしたらcurlとChrome両方が気を使って同じ実装をしている可能性は否定できませんが。)

## Google Chromeで0.0.0.0が使えない問題

現在のGoogle Chromeの検索窓に0.0.0.0を入力すると、`http://0.0.0.0/`にアクセスしに行きますが、その挙動が問題になったissueがあり、その時のやり取りが面白かったので載せておきます。現在はfixしています。
https://bugs.chromium.org/p/chromium/issues/detail?id=428046

要約すると、
1. `0.0.0.0`を入力するとnavigate(アドレスにアクセスする挙動)ではなくsearch(Googleで検索)するようにChromeチームが挙動を変更。なぜなら`0.0.0.0`はnon-routable addressだから。
2. Webサーバの表示`Server address: http://0.0.0.0:4000/`を信じた人や、`0.0.0.0`は今まで慣習で使っているので使えるべきだと主張する人が検索窓の挙動をnavigateに戻すように主張
3. Chromeチームは「そりゃWebサーバは`0.0.0.0`でLISTENしていると言うよ。それは正しい。でも`0.0.0.0`にアクセスしろなんて言ってないよね?」と反論
4. Chromeに`0.0.0.0`でアクセスできなくて困ると主張する人が湧き出てくる
5. Chromeチームが折れて`0.0.0.0`だけnavigateに挙動を修正。 `0.0.0.1`などはsearchのまま

とのことで、何も考えず`0.0.0.0`を検索窓に入力しても動くのはこのissueのおかげだったりします。

# Linux kernel

同僚に軽く話したところそれはもっと下のOSのレイヤで処理されているのではとのアドバイスを貰い、linux kernelを漁りました。まさかlinux kernelを読むことになる日が来ようとは。

Linux kernelは[ここ](https://www.kernel.org/)

TCP/IP周りは[このあたり](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/ipv4?h=v5.1-rc3)で、実際に0.0.0.0を127.0.0.1に変換している部分は`route.c`の中にありました。

```c:route.c
	if (!fl4->daddr) {
		fl4->daddr = fl4->saddr;
		if (!fl4->daddr)
			fl4->daddr = fl4->saddr = htonl(INADDR_LOOPBACK);
		dev_out = net->loopback_dev;
		fl4->flowi4_oif = LOOPBACK_IFINDEX;
		res->type = RTN_LOCAL;
		flags |= RTCF_LOCAL;
		goto make_route;
	}
```

大雑把に見ると、**もし宛先アドレスが`0.0.0.0`だったら送信元アドレスか127.0.0.1を宛先に代入する** 処理をしているようです。

fl4はflowi4という構造体で、`flow.h`に定義されています。
fl4->saddrとfl4->daarは`__be32`という型を持ちますが、これは`__u32`のtypedefで、unsigned(符号なし)32bit変数のことらしいです。

```c:flow.h
struct flowi4 {
	__be32			saddr;
	__be32			daddr;
```

IPv4が32bitなので、おそらくIPｖ４の32bitをそのまま変数に突っ込んでるものと思われます。ということは`0.0.0.0`は0ですね。

- `if (!fl4->daddr) `: C言語のif節では0をfalseとするため、この条件分岐は宛先アドレスが`0.0.0.0`か否かを判定していると考えられます
- `fl4->daddr = fl4->saddr = htonl(INADDR_LOOPBACK);`: 宛先アドレス, 送信元アドレスにINADDR_LOOPBACK(`127.0.0.1`)を代入しています


というわけで、宛先`0.0.0.0`を`127.0.0.1`に書き換えているのは(Linuxの場合は)kernelのroute.cであることがわかりました。

# まとめ

- Webサーバでよく見る`0.0.0.0`はこのホストのすべてのインターフェースでLISTENするという意味
- 宛先`0.0.0.0`は無効なアドレスなので、何も考えずにブラウザに`0.0.0.0:8000`のように入れるのは誤り
- ただしOSによっては`0.0.0.0`を`127.0.0.1`にルーティングしていることがあるため表面上は問題ないように見える(CORSには引っかかるので混同していると痛い目見るかも)

# 参考

https://christina04.hatenablog.com/entry/bind-advertise-address
https://bugs.chromium.org/p/chromium/issues/detail?id=428046
https://superuser.com/questions/949428/whats-the-difference-between-127-0-0-1-and-0-0-0-0