0️⃣

ping 0 のナゾ

2025/03/05に公開

はじめに

ある日、トチ狂ったみかんちゃんは次のようなコマンドを実行しました。

$ ping 0

すると、このコマンドは不思議なことに、自分自身に向けて ICMP パケットを投げ始めました。

$ ping 0
PING 0 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.076 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.090 ms
64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.097 ms
64 bytes from 127.0.0.1: icmp_seq=4 ttl=64 time=0.121 ms
... SNIP ...

これは奇妙なことですから、これについて少しだけ調べてみましょう。

環境

以下の環境で動作を確認しています。

$ uname -a
Linux mandarin.local.kusaremkn.com 6.11.0-17-generic #17~24.04.2-Ubuntu SMP PREEMPT_DYNAMIC Mon Jan 20 22:48:29 UTC 2 x86_64 x86_64 x86_64 GNU/Linux
$ cat /etc/os-release 
PRETTY_NAME="Ubuntu 24.04.2 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"
VERSION="24.04.2 LTS (Noble Numbat)"
VERSION_CODENAME=noble
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=noble
LOGO=ubuntu-logo
$ ping -V
ping from iputils 20240117
libcap: yes, IDN: yes, NLS: no, error.h: yes, getrandom(): yes, __fpending(): yes

そもそも 0 ってなんだよ

まずは ping 00 の部分です。 これは立派に認められた IPv4 アドレスの記法の一つです。 現代の IPv4 アドレスは、例えば、198.51.100.35 のように 10 進数をドットで区切って 4 つ並べる記法がよく用いられます。 しかし、実際にはこれらの数は必ずしも 10 進数である必要はなく、また 4 つ並べられる必要もありません。 詳細な説明は inet_aton(3) の man page に書いてあります[1]。 例えば、198.51.25635198.3367971198.51.100.35 と全く同じアドレスを表していますし、0306.51.0x642333252567390xC6336423 も同じアドレスを表しています。

これらを踏まえると、IPv4 アドレス 00.0.0.0 に対応していることがわかります。 しかし、ping は 127.0.0.1 に ICMP パケットを投げたと言っているため、奇妙です。

本当に 127.0.0.1 に投げているのか

実際に投げられたパケットは 0.0.0.0 を目指しているにも拘わらず、表示の部分がイタズラをしているだけかもしれません。 実際のところを確かめるため、パケットをキャプチャしてみましょう。 次のように実行して、別のセッションから ping 0 を実行します。

# tcpdump -vvv -x -n -i lo icmp
tcpdump: listening on lo, link-type EN10MB (Ethernet), snapshot length 262144 bytes
14:21:05.540640 IP (tos 0x0, ttl 64, id 41110, offset 0, flags [DF], proto ICMP (1), length 84)
    127.0.0.1 > 127.0.0.1: ICMP echo request, id 7372, seq 1, length 64
	0x0000:  4500 0054 a096 4000 4001 9c10 7f00 0001
	0x0010:  7f00 0001 0800 6ed9 1ccc 0001 41df c767
	0x0020:  0000 0000 9c3f 0800 0000 0000 1011 1213
	0x0030:  1415 1617 1819 1a1b 1c1d 1e1f 2021 2223
	0x0040:  2425 2627 2829 2a2b 2c2d 2e2f 3031 3233
	0x0050:  3435 3637
14:21:05.540675 IP (tos 0x0, ttl 64, id 41111, offset 0, flags [none], proto ICMP (1), length 84)
    127.0.0.1 > 127.0.0.1: ICMP echo reply, id 7372, seq 1, length 64
	0x0000:  4500 0054 a097 0000 4001 dc0f 7f00 0001
	0x0010:  7f00 0001 0000 76d9 1ccc 0001 41df c767
	0x0020:  0000 0000 9c3f 0800 0000 0000 1011 1213
	0x0030:  1415 1617 1819 1a1b 1c1d 1e1f 2021 2223
	0x0040:  2425 2627 2829 2a2b 2c2d 2e2f 3031 3233
	0x0050:  3435 3637
... SNIP ...

ICMP パケット(IP パケット)の先頭 20 オクテットの部分が IP ヘッダです。 IP ヘッダは次のような構造をもちます[2]。 0 から数えて 12 オクテット目に送信元アドレス、16 オクテット目に送信先アドレスが記述されています。

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version|  IHL  |Type of Service|          Total Length         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Identification        |Flags|      Fragment Offset    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Time to Live |    Protocol   |         Header Checksum       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       Source Address                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Destination Address                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Options                    |    Padding    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

キャプチャされたパケットを眺めてみると、送信元も送信先も 0x7f000001 が指定されており、これは 127.0.0.1 です。 うん、確かに投げられたパケットは 127.0.0.1 を目指しているようです。

実装を見てみる

さて、とうとう実装を見てみる必要がありそうです。 ソースコードは GitHub に公開されています。 見ましょう。

https://github.com/iputils/iputils/tree/20240117

プログラムの開始点である main()306 行目にあります。 オプションの解析や引数のチェックを経て、実際の ping の処理は 741 行目にある ping4_run() で捌くようです。 パケットの送出処理は 1035 行目で呼び出されている main_loop() の内部にあります。

もっと詳細に見てみましょう。 送信元アドレスや送信先アドレスを指定する部分を眺めていきます。

  • 776 行目: 送信先表示 target を IPv4 アドレスとして解釈してみたり、名前解決してみたりして送信先アドレス rts->whereto に設定する

  • 814 行目: 送信元アドレス rts->source.sin_addr.s_addr が 0(未指定)なら

    • 816 行目: 検査用の UDP ソケット probe_fd を作り
    • 817 行目: dst を送信先アドレス rts->whereto に設定し
    • 837 行目: probe_fddstconnect() する
    • 856 行目: probe_fd の接続されている先のアドレスを rts->source に設定する
  • 886 行目: 送信先アドレス rts->whereto.sin_addr.s_addr が 0(未指定)なら送信元アドレス rts->source.sin_addr.s_addr と同じ値に設定される

ここで、ping 0 つまり target の値として "0" が指定された場合を考えてみると、次のようになります。

  • "0" を IPv4 として解釈し、送信先アドレス 0 を得る。
  • 送信元を指定していないため、送信元を確定させる。
    • 送信元アドレスに送信先アドレス 0 を指定し、そこへ connect() する。
    • これに接続されている先のアドレスを送信元として設定する。
  • 送信先アドレスが未指定の場合と同じ値 0 であるから、送信先アドレスが送信元アドレスと同じ値に設定される。

なるほど。 確かに私たちの指定した送信先アドレス 0 とは異なるものが送信先アドレスに設定されていることがわかります。

オプション -I interface を利用して適当な送信元アドレスを指定し、送信先に 0 を指定すると送信元の値が使われていることを確かめられます。

$ ping -I 172.16.16.61 0
PING 0 (172.16.16.61) from 172.16.16.61 : 56(84) bytes of data.
64 bytes from 172.16.16.61: icmp_seq=1 ttl=64 time=0.130 ms
64 bytes from 172.16.16.61: icmp_seq=2 ttl=64 time=0.124 ms
64 bytes from 172.16.16.61: icmp_seq=3 ttl=64 time=0.097 ms
... SNIP ...

犯人探し

上の処理を抜き出して問題を再現するプログラムを以下に示します。 適当な UDP ソケットを作り、0connect() した上で getsockname() するとループバックアドレスが得られます。 この現象はソケットを 0connect() しないと得られません。

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <err.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int
main(void)
{
	struct sockaddr sa;
	struct sockaddr_in sin;
	socklen_t slen;
	int sock;
	char name[16];

	sock = socket(AF_INET, SOCK_DGRAM, 0);
	if (sock == -1)
		err(1, "socket");

	sin.sin_family      = AF_INET;
	sin.sin_port        = 9;
	sin.sin_addr.s_addr = 0;	/* 宛先は 0 */
	if (connect(sock, (struct sockaddr *)&sin, sizeof(sin)) == -1)
		err(1, "connect");

	slen = sizeof(sa);
	if (getsockname(sock, &sa, &slen) == -1)
		err(1, "getsockname");

	(void)memcpy(&sin, &sa, sizeof(sin));
	if (inet_ntop(AF_INET, &sin.sin_addr, name, sizeof(name)) == NULL)
		err(1, "inet_ntop");
	(void)puts(name);

	(void)close(sock);

	return 0;
}

おわりに

おわりです。

ソケットを 0connect() すると、接続先は常に 127.0.0.1 になるのか気になるところです。 個人的にはならないと思います。 恐らく、最初のネットワークインタフェースの IPv4 アドレスが選択されているのではないでしょうか。

追記: 2025-03-12

ループバックデバイスが最初のネットワークインタフェースではないような FreeBSD 上で 0connect() し、getsockname() してみたところ、ループバックアドレス 172.0.0.1 が得られました。 うーむ、きちんと調べる必要がありそうです。


他の実装

他の実装では、このような挙動は認められませんでした。

FreeBSD
% uname -a
FreeBSD sanboukan.local.kusaremkn.com 14.2-RELEASE-p1 FreeBSD 14.2-RELEASE-p1 GENERIC amd64
% freebsd-version -kru
14.2-RELEASE-p1
14.2-RELEASE-p1
14.2-RELEASE-p2
% ping 0
PING 0 (0.0.0.0): 56 data bytes
^C
--- 0 ping statistics ---
1 packets transmitted, 0 packets received, 100.0% packet loss
Windows 10
>ver

Microsoft Windows [Version 10.0.26100.3194]

>ping 0

0.0.0.0 に ping を送信しています 32 バイトのデータ:
ping: 転送に失敗しました。一般エラーです。
ping: 転送に失敗しました。一般エラーです。
ping: 転送に失敗しました。一般エラーです。
ping: 転送に失敗しました。一般エラーです。

0.0.0.0 の ping 統計:
    パケット数: 送信 = 4、受信 = 0、損失 = 4 (100% の損失)、

脚注
  1. https://ja.manpages.org/inet_aton/3 ↩︎

  2. https://datatracker.ietf.org/doc/html/rfc791 ↩︎

Discussion