Edited at

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


はじめに

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


tl;dr



  • 0.0.0.0を宛先に使うのは誤り

  • ただしOSによっては 127.0.0.1 に到達するので支障がなかったりする


想定読者

0.0.0.0127.0.0.1の違いをすぐに答えられない人が対象です。

ネットワークな人はわかっていることだと思うのでブラウザバックしてもらって構いません。

強い人は間違えているところコメントください。


環境

Ubuntu 16.04を利用します。

$ 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が見られます。

$ 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 + クリック)、用意したコンテンツを確認。

スクリーンショット 2019-04-02 午後11.46.38.png

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

localhost.png

今回の挙動は図のような動きになります。

1. pythonのhttpサーバがLISTENしている

2. 同一マシン上からブラウザでhttp://0.0.0.0:8000/にHTTP GETリクエストを飛ばします

3. リクエストを受けたpythonはindex.htmlファイルをステータス200で返します

4. ブラウザはHTTP Responseを画面に描画します


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 のはず。


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 でサーバを起動するとどちらのインターフェースでも到達可能になります。

$ 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/' # 1つ目のインターフェースの応答確認
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/とすることもできます。


/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にアクセスすると良さそうです。

$ 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はここ

TCP/IP周りはこのあたりで、実際に0.0.0.0を127.0.0.1に変換している部分はroute.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変数のことらしいです。


flow.h

struct flowi4 {

__be32 saddr;
__be32 daddr;

IPv4が32bitなので、おそらくIPv4の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.0127.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.0127.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