はじめに
この記事は2019年4月時点で調べたものをベースにしています。将来的に変わるかもしれません。
tl;dr
-
0.0.0.0
を宛先に使うのは誤り - ただしOSによっては
127.0.0.1
に到達するので支障がなかったりする
想定読者
0.0.0.0
と127.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 + クリック)、用意したコンテンツを確認。
hello world!と表示されましたね。
今回の挙動は図のような動きになります。
- pythonのhttpサーバがLISTENしている
- 同一マシン上からブラウザで
http://0.0.0.0:8000/
にHTTP GETリクエストを飛ばします - リクエストを受けたpythonは
index.html
ファイルをステータス200で返します - ブラウザはHTTP Responseを画面に描画します
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
は自身を表すアドレスではないというコメントを見て、言われてみれば確かにそうなので追記しておきます。
A datagram sent by a higher-level protocol to an address anywhere within this block loops back inside the host.
obsoletedですがRFC5735の記述でこのように書かれているので、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
でサーバを起動するとどちらのインターフェースでも到達可能になります。
$ 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/
とすることもできます。
$ 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"
ここで注目したいのが、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
要約すると、
-
0.0.0.0
を入力するとnavigate(アドレスにアクセスする挙動)ではなくsearch(Googleで検索)するようにChromeチームが挙動を変更。なぜなら0.0.0.0
はnon-routable addressだから。 - Webサーバの表示
Server address: http://0.0.0.0:4000/
を信じた人や、0.0.0.0
は今まで慣習で使っているので使えるべきだと主張する人が検索窓の挙動をnavigateに戻すように主張 - Chromeチームは「そりゃWebサーバは
0.0.0.0
でLISTENしていると言うよ。それは正しい。でも0.0.0.0
にアクセスしろなんて言ってないよね?」と反論 - Chromeに
0.0.0.0
でアクセスできなくて困ると主張する人が湧き出てくる - 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
の中にありました。
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変数のことらしいです。
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.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