pingの世界の徒歩旅行, そして鍋

## 前記

こんにちは。[42 Tokyo](https://42tokyo.jp/)というエンジニア養成機関の [2023 年度アドベントカレンダー](https://qiita.com/advent-calendar/2023/42tokyo) 16日目も担当します、在校生のyokawadaと申します。

今日は`ping`コマンドについてのお喋りです。鍋情報もあるよ。

:::note warn

以下、IPといえばIPv4のことです。
ICMPも同様。

:::

## pingコマンド

自分のコンピュータが、指定したネットワークホストとインターネット的に繋がっていることを確認するためのコマンド。

```bash:macOSでpingを打ってみる
$ ping google.com
PING google.com (142.250.196.142): 56 data bytes
64 bytes from 142.250.196.142: icmp_seq=0 ttl=120 time=5.812 ms
64 bytes from 142.250.196.142: icmp_seq=1 ttl=120 time=5.579 ms
64 bytes from 142.250.196.142: icmp_seq=2 ttl=120 time=5.223 ms
64 bytes from 142.250.196.142: icmp_seq=3 ttl=120 time=5.541 ms
64 bytes from 142.250.196.142: icmp_seq=4 ttl=120 time=5.812 ms
64 bytes from 142.250.196.142: icmp_seq=5 ttl=120 time=8.861 ms
64 bytes from 142.250.196.142: icmp_seq=6 ttl=120 time=5.688 ms
^C # <- ほっとくと延々と続くので、ここで Ctrl-C を押して終わらせた
--- google.com ping statistics ---
7 packets transmitted, 7 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 5.223/6.074/8.861/1.153 ms
$
```

細かい用語説明は後ほど行うとして、この結果から読み取れることを列挙してみる:

- `ping`コマンドを実行した。
- 宛先ホストは`google.com`で, これはIP(v4)アドレス`142.250.196.142`に解決された。
- `google.com`に向かって送信されるpingパケットのデータ部分が56バイトある。
- IPアドレス`142.250.196.142`から返事が届いた。
    - 返事のパケットのデータ部分はいずれも64バイトあった。
    - 返事のパケットの`icmp_seq`は0から1ずつ増えていく。
    - 返事のパケットの`ttl`はすべて120だった。
    - 送ったpingパケットすべてに対して返事が届いた。


pingコマンドが繋がっていることを確認するために使う主要な道具は**ICMPメッセージ**で、その中でも次の3種類が使われる:

- ICMP Echo
- ICMP Echo Reply
- ICMP Time Exceeded

最後の"Time Exceeded"は「繋がっていないこと」あるいは「遠い」ことを表すのに使われる。

## ICMP: Internet Control Message Protocol

https://datatracker.ietf.org/doc/html/rfc792

ICMPは通信プロトコルの一種。

IP(Internet Protocol)の上で動作するため、TCPやUDPと同じ位置づけのプロトコルであるはずだが、IPとは実質その一部と呼べるほど固く結びついている。

なお、ICMPのRFCはかなり短い(表で行数を稼いでいる)ので、気軽に読むことができる。

### ICMPの目的

ICMPの目的は、インターネットの通信過程で問題が発生した時、それを通知することにある。

たとえばあるホストが、受け取ったTCPパケットを「宛先ポートが開いていない」という理由で破棄する際、
送信元に対して`Destination Unreachable`(宛先到達不能)タイプのICMPメッセージを送り返す。
さらに、到達できない理由を示すために「コード」の値を3(=ポート到達不能)にセットする。

### ICMP Echo と Echo Reply

大半のICMPメッセージは、何かしらのイベントに対するリアクションとして送信される。
しかし例外的に、能動的に送信されるICMPメッセージもある。

ICMP **Echo**(あるいは**Echo Request**)はその例外の1つで、
「宛先ホストがこのメッセージを受け取った時は、送信元に対してICMP **Echo Reply**メッセージを送り返してほしい」
という意味を持つ。

### ICMP Time Exceeded と TTL(Time To Live)

"Time Exceeded"、つまり"時間切れ"という名称からして、何らかのタイムアップ時に返送されるICMPメッセージであろうことはなんとなく予想できるが、直接的な時間切れを意味しているわけではない。

Time Exceededが発生するのは「IPヘッダのTTL値がゼロになった時」である。
IPデータグラムは、独立したネットワーク間をバケツリレーによって旅しながら、IPヘッダに書かれた宛先アドレスへの到達を目指す。
ネットワークからネットワークへバケツが手渡されるごとに、IPヘッダのTTL値が1つずつ減少する。
そして宛先アドレスに到達する前にTTL値がゼロになってしまうと、IPデータグラムは破棄される。

TTL値は、IPデータグラムを送信する側が自由に決めることができる。
よって、TTL値を色々変えてIPデータグラムを送り、Time Exceededが返ってくるかどうかを調べることで、送信元と宛先との「ネットワーク的な距離」を測ることができる。


<details>

<summary> <strong>余談: TTL値の元々の意味</strong> </summary>

IPv4のRFCには:

>  The time is measured in units of seconds,

とある。つまりTTL値は文字通り秒単位のTimeであるわけだが、RFCでは続けて:

> but since every module that processes a datagram must decrease the TTL by at least one even if it process the datagram in less than a second, the TTL must be thought of only as an upper bound on the time a datagram may exist.

つまり、あるネットワークを1秒以内に通過したとしても、TTL値は必ず1減少する。
よってTTL値はネットワークを移るごとに**少なくとも**1減ることになる。
</details>

## pingパケットを見てみよう、Wiresharkで。

`google.com`に向かって, 1回だけEcho Requestを送信し、1個だけEcho Replyを受け取る。

```bash
$ ping -c1 google.com
PING google.com (142.250.196.110): 56 data bytes
64 bytes from 142.250.196.110: icmp_seq=0 ttl=61 time=8.578 ms

--- google.com ping statistics ---
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 8.578/8.578/8.578/0.000 ms
$
```

この通信を[Wireshark](https://www.wireshark.org/download.html)でキャプチャする。

<details>
<summary>一応、手順</summary>

Wiresharkを起動するとまずこの画面になると思われる:

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102690/d7e1bd8c-0876-4b9f-3e23-cbd955f5dd88.png" width=600>

ネットに接続しているインターフェースを選ぶ。Wi-Fi接続ならWi-Fiと書いてあるものを選んで間違いないはず。

選ぶとキャプチャーが始まる。
最初は全ての通信をキャプチャーするので行がすごい勢いで増えていく。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102690/d94f295d-edf0-4407-f76c-6bb29872425e.png" width=600>

慌てず騒がず、フィルター欄に`icmp`と打ち込んでEnter.

</details>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102690/5f48497b-fb87-e50a-f97b-838f1473f821.png)

Echo Requestデータグラムの中身を見てみる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102690/def08a43-b102-722c-8162-1931df474cad.png)

右側はデータグラムの中身を16進表示したもので、
左側にはその各部分がどういう意味なのかが表示されている。
トグルを開いていけば詳細に説明してくれるが少々見づらいので、別途図解したものがこちら:

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102690/586764d8-31f3-e50b-e9fd-5c4365029c5a.png)

この図解では、データグラムを大きく4つの部分に分けている:

- Ethernet MACヘッダ: 14オクテット
- IPヘッダ: 20オクテット
- ICMPヘッダ: 8オクテット
- ICMPペイロード: 48オクテット

関心があるのはIPヘッダ以降の部分なので、そちらのみ簡単に説明する。

### IPヘッダ

- バージョン
    - IPv4なので, 当然4
- ヘッダサイズ
    - IPヘッダのサイズを表すが、4ビットしかないので, `IPヘッダのオクテット数 / 4`という形で表す。
- IPデータグラムのサイズ
    - これはヘッダではなくデータグラム全体, つまりIPヘッダの先頭からデータグラムの終わりまでの長さをオクテット単位で表す。
- TTL
    - Time To Liveの値
- 上位プロトコル
    - IPの上に何が載っているか。
- 送信元アドレス
- 宛先アドレス
    - いずれもIPv4アドレス。

### ICMPヘッダ

- タイプ
    - Echoなら8, Echo Replyなら0
- コード
    - Echo, Echo Replyどちらの場合も0
    - タイプは大分類、コードは小分類。
- 識別子
- シーケンス番号
    - Echo Replyの識別子とシーケンス番号は対応するEchoと同じになる。
    - Echoの識別子には`ping`プロセスのプロセスID, シーケンス番号には0から1ずつ増加する整数を充てることが多い。
    - そうしておくと、`ping`が受信したEcho Replyについて、以下のようなチェックができる:
         - 識別子が自分のプロセスIDと違う → 自分と関係ないのでスルー
         - シーケンス番号が今まで送ったEchoの数より大きい → 明らかにおかしいのでスルー
         - シーケンス番号がすでに受信したEcho Replyと同じ → エラー表示

ICMPヘッダはサイズに関する情報を直接持たないので、ペイロードサイズはIPヘッダの情報から自力で計算する必要がある。

### ICMPペイロード

ICMPデータグラムのペイロードのうち、最初の8オクテットには`timeval`構造体をそのまま埋め込む形でタイムスタンプが入ることが多い。

<details>
<summary> timeval構造体 </summary>

```c
struct timeval {
    long tv_sec;
    long tv_usec;
};
```

</details>

タイムスタンプの値としては**Echoの送信時刻**が使われる。

---

今度はEcho Replyの中身。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102690/fb045ff1-cf11-bc9a-740e-cb1b425c6b97.png)

こちらも図解してみた。
Echo Requestと対比すべき箇所は文字色赤で示してある:

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102690/5dc47c09-cfe0-7e85-99e8-16ad7c223eab.png)

なんとなく感じが掴めただろうか？

## pingを再実装する

ここからは再実装について。
ただ全てを記しているには12月はあまりに短すぎるので、いくつかのトピックについて述べるにとどめる。

### Rawソケット

ICMPデータグラムを送るにはRawソケットを使うが、通常これは一般ユーザには使用が許可されていない。
しかし`ping`コマンドは通常ユーザにも問題なく使えるので、どこかに抜け道があることになる。

#### `setuid`

https://docs.oracle.com/cd/E19455-01/806-2718/secfiles-69/index.html

実行ファイルを、「実行したユーザ」ではなく「所有しているユーザ」の権限で実行することができる。
`ping`バイナリを`root`所有にして`setuid`しておけば、一般ユーザの`ping`実行時にも`root`の次権限で実行されることになる。

#### Capability `CAP_NET_RAW`

`setuid`は所有者の権限をすべて実行者に与えてしまう。
OSによっては、より粒度の小さい特権付与としてCapability(ケイパビリティ)機能がある。

https://gihyo.jp/admin/serial/01/linux_containers/0042

Linux系OSでRawソケットを使うためには、`CAP_NET_RAW` Capabilityがあればよい。

#### `net.ipv4.ping_group_range`

*よく知りません*

#### データグラムソケット(`SOCK_DGRAM`)

そもそも送受信にRawソケットを使わなければ問題は発生しない。

https://opensource.apple.com/source/network_cmds/network_cmds-543.260.3/ping.tproj/ping.c.auto.html

```c:ping.c
	if (getuid())
		s = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
	else
		s = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
```

ただしデータグラムソケットソケットを使うこの場合、プログラムからアクセスできる情報が制限される。
(IPヘッダがプログラムから見えなくなる、など。)

### ICMPデータグラムの送受信

`ping`コマンドにおいては

- 一定間隔でEchoを打つ
- いつ来るかわからないEcho Replyを待ち受ける

を同時に行う必要がある。

最も自然なアプローチは`select`等のイベント監視手段を使うことだが、
何らかの事情で使えない場合は、ソケットオプションで受信にタイムアウトを設定して頑張ることになる。

### ラウンドトリップタイム(往復時間)の計測

`ping`コマンドで表示されるラウンドトリップタイムの計算には、ICMP Echo Replyのペイロード中のタイムスタンプを使う。
Echo Replyが持っているタイムスタンプは「対応するICMP Echoの送信時刻」なので、これを現在時刻から差し引くことでラウンドトリップタイムを求める。

注意すべき点:

- Echo Replyが持っているタイムスタンプは「Echo Replyの送信時刻」ではない
- ICMPペイロードのサイズが小さい場合はタイムスタンプがないことがある
    - タイムスタンプ(`timeval`)の格納には8オクテット必要
- そもそもペイロードにタイムスタンプを入れることがICMPで定められているわけではない

### OSによる差異

IPヘッダとICMPヘッダの構造体はあらかじめ定義されているが、環境により名前が全然違うので、
たとえばLinux/macOS互換にしようとすると多少面倒なことになる。

また筆者が観察した限りでは、受信したデータグラムがOSによって改変されている場合がある。
IPヘッダの`Total Length`にはIPデータグラム全体のサイズが入ることになっているが、
macOSで受信したIPデータグラムの`Total Length`からは**IPヘッダのサイズが差し引かれている**ことがある。

#### IPタイムスタンプとICMPタイムスタンプ

*疲れてきたのでこの話はやめます。とりあえず、IPとICMPのタイムスタンプは別物で、`ping`コマンドにはどちらも関わりがあるとだけ。また後日書き足すかもしれません。*

## 後記

明日は @42yliu さんがvimについての記事を書く・・・vim出てくるの？

## 参考文献

### ICMP(for IPv4)のRFC

https://datatracker.ietf.org/doc/html/rfc792

### IP(v4)のRFC

https://datatracker.ietf.org/doc/html/rfc791

### 色々な`ping`コマンド

#### macOS

https://opensource.apple.com/source/network_cmds/network_cmds-606.40.2/ping.tproj/ping.c.auto.html

#### iputils

https://github.com/iputils/iputils/blob/master/ping/ping.c

#### Linux

https://github.com/torvalds/linux/blob/master/net/ipv4/ping.c

#### おれの

(並べるなよな・・・)

https://github.com/corvvs/ding

### `traceroute`

コンセプトはとても似ている、というかTTLを変えながら`ping`するだけじゃないか。
と思いきや、実はそれだけではないのが面白いところ。

https://ja.wikipedia.org/wiki/Traceroute


## 真冬のズボラ鍋レシピ - 常夜鍋

元々この記事のタイトルは「真冬のズボラ鍋レシピ」だったので、一応鍋レシピも書いておく。
冬は体温維持がなにより大事だからね。あったまるもの食べようね。

### 材料

- **Mandatory**
    - 豚肉薄切り - 適量
    - ほうれん草 - 適量
    - 豆腐 - 適量
        - 舌触り重視なら絹, 扱いやすさ重視なら木綿で
    - 生姜 - 1/8欠け程度
    - 料理酒 - 鍋の水量の半分
    - 粗塩 - 適当
        - 卓上塩は勿体無い
- **Bonus**
    - 豚肉薄切り → 豚バラ肉
    - ほうれん草 → 小松菜
    - 生姜 → にんにく
    - だし昆布 - 1枚
        - 味付けは塩・料理酒・生姜で十分だが、気分で昆布を足してもよい。
    - 椎茸 - 適量
        - だしをさらに出したい場合に。
    - 大根 - 適量
        - 冬感を出したい場合に。
    - 長ネギ - 適量
        - 甘みを出したい場合に。
    - ゆず胡椒
    - ポン酢醤油
        - つけだれ的なやつ。

### 手順

1. 土鍋に水を張り、料理酒を入れ、生姜の薄切りを沈めて火をかける。
    - だし昆布を入れるならここから。
    - 大根もあるならここから。
2. 水が温まってきたら豆腐を適当に切って入れる。
    - 派手に沸騰させない方が豆腐が壊れない。
    - 椎茸があるならここから。
3. ほうれん草を投入。
    - 火の通し具合はお好みだが、あまり煮過ぎないのをお勧めする。
4. 豚肉を手でほぐして投入。
    - 豚肉は煮込まず、火が入って脂が溶け出すくらいにしておく。
    - なので火を弱める。
    - 豚肉をほぐすのは地味に重要で、塊のままだと火が入りにくい。
5. 豚肉に火が入ったら、塩で味付けする。
    - 豚肉を入れると味が大きく変わるので、塩味を足すのは豚肉を入れてからにしておいた方が無難。
6. 完成！

スケジューリングさえ間違えなければ、完成までは20分程度。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102690/bb3d996b-db9b-9294-1719-7462fb0ffc7c.png" width=800>

料理におけるタスク間の依存関係はDAGになる(でないといつまで経っても完成しない)ので、各タスクの具体的な実施タイミングはその範囲で自由に設定できる。
ただし火を使う場合は「火の入り方」が絡むことを忘れずに・・・

### 完成イメージ

Bonus食材の大根がめちゃめちゃ目立っている。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102690/8c78a4bb-deb5-b287-f298-3d46a54682ad.png" width=600>

常夜鍋の本体は事後の雑炊という説が有力です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102690/0d1fbd52-3c89-b03b-7ec3-d793183297c0.png" width=400>