LoginSignup
7
1

More than 1 year has passed since last update.

Goでpingを実装する

Last updated at Posted at 2022-12-21

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

はじめに

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

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

ICMP

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

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

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

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
)

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

ICMP Echoリクエスト図解

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

このICMPパケットの構成はRFC792のページに掲載されている図がわかりやすかったため,以下に引用します.

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に掲載しています.

環境

  • 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の受信

こちらも PacketConnReadFrom で簡単に受信できます.

    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の新卒アドベントカレンダーに参加させていただくことになり,自分自身が「モノを作りながら」様々なことを学んできたところを振り返りつつ,それを発信できればとこのような記事を書かせていただきました.直接役に立つことは少ないかもしれませんが,誰かの何かにつながっていたら私はとても幸せです.

参考文献

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1