本記事は DeNA 23 新卒 Advent Calendar 2022 の22日目の記事となります.
はじめに
主に指定の宛先とのラウンドトリップタイムを計るときに使う ping というソフトウェアがあります.主に回線状況の確認の目的で使われることの多いこのソフトウェア,OS問わずに利用可能であることもあり,使用経験のある方も多いのではないでしょうか.
私も時折使っていたのですが,内部的にどのように通信が行われているのかというところをしっかりとは理解できていなかったところがあります.
本記事はその理解を深めることを目指し,周辺の知識を復習しながらGoによってpingを実装したことをまとめたものです.
ICMP
pingはICMP(Internet Control Message Protocol)というRFC792で定義されたプロトコルを利用します.
ラウンドトリップタイムを計測するpingのように,IP関連のデバッグに使われるプロトコルです.
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
です.名前の通り,ホストが木霊を返すかのように受け取ったパケットをそのまま送り返すのがこれらのタイプになります.
簡単なEcho通信の様子は上のようになります.この通り,Type以外は全く同じものを返します.これが今回pingを実装する上で最も重要になる部分です.
このICMPパケットの構成はRFC792のページに掲載されている図がわかりやすかったため,以下に引用します.
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に掲載しています.
環境
- Windows 10 Pro 22H2 + Windows Subsystem for Linux 2(Ubuntu 20.04.2 LTS)
- Golang 1.17.1
実装
Goにはicmpというパッケージが存在しますので,基本的にはこちらを使っていきます.
main.go では色々と書いていますが,メインは try
関数です.*icmp.PacketConn
と 宛先のnet.IP
を受け取って,標準出力に何かしらの結果を出力します.
ICMPメッセージ
icmp.Message
構造体で送信するEchoリクエストを作成します.指定している項目は基本的にICMPパケットの構成で紹介した事柄そのままです.ここではicmpパッケージのリファレンスを参考にしました.チェックサムは自動で計算してくれるようです.
Dataには送信時間をByte化したものを入れています.これにより,Echo Replyが返ってきたときにはその受信時間とDataに含まれる送信時間の差を確認することによって,ラウンドトリップタイムが計算できるというからくりです.
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秒くらいにしてあげています.
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
で簡単に受信できます.
rb := make([]byte, 1500)
n, _, err := c.ReadFrom(rb)
そして適宜エラーハンドリングをしてあげて,先述した通り「受信時間 - 送信時間」を出力するような形で完成です.icmp
パッケージが様々な機能がついていますので,やりたい仕様にしやすいと思います.今回はシンプルな実装となるようにしています.
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の新卒アドベントカレンダーに参加させていただくことになり,自分自身が「モノを作りながら」様々なことを学んできたところを振り返りつつ,それを発信できればとこのような記事を書かせていただきました.直接役に立つことは少ないかもしれませんが,誰かの何かにつながっていたら私はとても幸せです.