Goでpingを実装する

本記事は DeNA 23 新卒 Advent Calendar 2022 の22日目の記事となります．

https://qiita.com/advent-calendar/2022/dena-23-shinsotsu

# はじめに

主に指定の宛先とのラウンドトリップタイムを計るときに使う **ping** というソフトウェアがあります．主に回線状況の確認の目的で使われることの多いこのソフトウェア，OS問わずに利用可能であることもあり，使用経験のある方も多いのではないでしょうか．
私も時折使っていたのですが，内部的にどのように通信が行われているのかというところをしっかりとは理解できていなかったところがあります．

本記事はその理解を深めることを目指し，周辺の知識を復習しながらGoによってpingを実装したことをまとめたものです．

# ICMP

pingはICMP(Internet Control Message Protocol)というRFC792で定義されたプロトコルを利用します．

https://www.rfc-editor.org/rfc/rfc792.html

ラウンドトリップタイムを計測するpingのように，IP関連のデバッグに使われるプロトコルです．

ICMPには様々な機能があり，これは「タイプ」で区別されます．

```go:ICMPのタイプ
// https://pkg.go.dev/golang.org/x/net/ipv4#ICMPType
const (
	ICMPTypeEchoReply              ICMPType = 0  // Echo Reply
	ICMPTypeDestinationUnreachable ICMPType = 3  // Destination Unreachable
	ICMPTypeRedirect               ICMPType = 5  // Redirect
	ICMPTypeEcho                   ICMPType = 8  // Echo
	ICMPTypeRouterAdvertisement    ICMPType = 9  // Router Advertisement
	ICMPTypeRouterSolicitation     ICMPType = 10 // Router Solicitation
	ICMPTypeTimeExceeded           ICMPType = 11 // Time Exceeded
	ICMPTypeParameterProblem       ICMPType = 12 // Parameter Problem
	ICMPTypeTimestamp              ICMPType = 13 // Timestamp
	ICMPTypeTimestampReply         ICMPType = 14 // Timestamp Reply
	ICMPTypePhoturis               ICMPType = 40 // Photuris
	ICMPTypeExtendedEchoRequest    ICMPType = 42 // Extended Echo Request
	ICMPTypeExtendedEchoReply      ICMPType = 43 // Extended Echo Reply
)
```

いくつかあるタイプの中でも，今回用いるのは `Echo` と `Echo Reply` です．名前の通り，ホストが木霊を返すかのように受け取ったパケットを**そのまま**送り返すのがこれらのタイプになります．

![ICMP Echoリクエスト図解](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191019/dd5e6a70-a4fe-4937-0284-3490b2349313.png)

簡単なEcho通信の様子は上のようになります．この通り，Type以外は全く同じものを返します．これが今回pingを実装する上で最も重要になる部分です．

このICMPパケットの構成は[RFC792](https://www.rfc-editor.org/rfc/rfc792.html)のページに掲載されている図がわかりやすかったため，以下に引用します．

```txt:ICMPパケットの構成
    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
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |     Type      |     Code      |          Checksum             |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |           Identifier          |        Sequence Number        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |     Data ...
   +-+-+-+-+-
```
 ざっくりと各構成要素について説明すると，以下のようになります．

|構成要素|説明|
|-|-|
|Type|先述の通り|
|Code|常に0，Echoでは使わない|
|Checksum|整合性の確認のためのチェックサム|
|Identifier|EchoとReplyの対応付けに使用可能な識別子|
|Sequence Number|EchoとReplyの対応付けに使用可能なシーケンス番号|
|Data|リクエスト側で指定し，リプライではそのまま返る|

pingがやっていることは非常に単純で，このプロトコルに従いICMPのEchoリクエストを行い，それがEcho replyとして返ってくるまでの時間を計測しているということになります．

# pingの実装

このICMPを用いてGoでのpingの実装を行っていきたいと思います．pingには実はいろいろなオプションがありますが，今回はIPv4で指定したアドレスにEchoリクエストを行い，簡単にラウンドトリップタイムを計測するところを目指します．

簡単な実装ではありますが，本実装はGitHubに掲載しています．

https://github.com/pgDora56/PinGo

## 環境
- Windows 10 Pro 22H2 + Windows Subsystem for Linux 2(Ubuntu 20.04.2 LTS)
- Golang 1.17.1

## 実装
Goにはicmpというパッケージが存在しますので，基本的にはこちらを使っていきます．

https://pkg.go.dev/golang.org/x/net/icmp

[main.go](https://github.com/pgDora56/PinGo/blob/master/main.go) では色々と書いていますが，メインは `try` 関数です．`*icmp.PacketConn` と 宛先の`net.IP`を受け取って，標準出力に何かしらの結果を出力します．

### ICMPメッセージ
`icmp.Message` 構造体で送信するEchoリクエストを作成します．指定している項目は基本的にICMPパケットの構成で紹介した事柄そのままです．ここではicmpパッケージのリファレンスを参考にしました．チェックサムは自動で計算してくれるようです．

Dataには送信時間をByte化したものを入れています．これにより，Echo Replyが返ってきたときにはその受信時間とDataに含まれる送信時間の差を確認することによって，ラウンドトリップタイムが計算できるというからくりです．

```golang
    msg := icmp.Message{
		Type: ipv4.ICMPTypeEcho,
		Code: 0,
		Body: &icmp.Echo{
			ID: os.Getpid() & 0xffff, Seq: 1,
			Data: result,
		},
	}
```



これを`Marshal`してByte列に変換して，実際に送信します．

`PacketConn`のメソッドである`WriteTo`でMessageのByte列と宛先IPを指定してあげるだけの優しい設計になっています．`SetDeadline`でタイムアウトは5秒くらいにしてあげています．

```golang
    msgBytes, err := msg.Marshal(nil)
	if err != nil {
		panic(err)
	}
	if _, err := c.WriteTo(msgBytes, &net.IPAddr{IP: ip}); err != nil {
		panic(err)
	}

	c.SetDeadline(time.Now().Add(time.Second * 5))
```

### Echo Replyの受信

こちらも `PacketConn` の `ReadFrom` で簡単に受信できます．

```golang
    rb := make([]byte, 1500)
	n, _, err := c.ReadFrom(rb)
```

そして適宜エラーハンドリングをしてあげて，先述した通り「受信時間 - 送信時間」を出力するような形で完成です．`icmp`パッケージが様々な機能がついていますので，やりたい仕様にしやすいと思います．今回はシンプルな実装となるようにしています．

```golang
	if err != nil {
		fmt.Println("Receive Failed:", err.Error())
	} else {
		rm, err := icmp.ParseMessage(ipv4.ICMPTypeEcho.Protocol(), rb[:n])
		if err == nil && rm.Type == ipv4.ICMPTypeEchoReply {
			echo, ok := rm.Body.(*icmp.Echo)
			if !ok {
				fmt.Println("Body isn't echo:", err.Error())
			} else {
				t, _ := binary.Varint(echo.Data)
				fmt.Printf("%d ms\n", time.Now().UnixMilli()-t)
			}
		} else {
			fmt.Println("Parse Failed:", err.Error())
		}
	}
```

# おわりに
私は技術を理解するときによく車輪の再発明っぽいことをするのが好きで，本記事で行ったpingの再実装もその一つといえば一つになります．

世には当然のように便利なものが溢れかえっていますが，それを改めて作ることでまた新たな発見があって，別の事柄に応用できたりできなかったりします．ただ，どちらにせよ得るものは必ずあると信じています．

今回「モノづくり」を大切にしているDeNAの新卒アドベントカレンダーに参加させていただくことになり，自分自身が「モノを作りながら」様々なことを学んできたところを振り返りつつ，それを発信できればとこのような記事を書かせていただきました．直接役に立つことは少ないかもしれませんが，誰かの何かにつながっていたら私はとても幸せです．


# 参考文献
- [Software Design 2021年5月号 第1特集「ハンズオンTCP/IP」](https://gihyo.jp/magazine/SD/archive/2021/202105), 技術評論社, 2021/5
- [RFC 792: INTERNET CONTROL MESSAGE PROTOCOL](https://www.rfc-editor.org/rfc/rfc792.html)