go实现ping

发布时间 2023-09-04 17:54:35作者: redrobot

 

ping 是一个经常被用来检查主机间连通性的工具, 它基于 ICMP 协议实现, 基本原理很简单: 本机给远程机器发送 ICMP 报文, 远程主机接收到 ICMP 报文后便会回复一个类似的 ICMP 报文; 当本机接收到回复后变认为远程主机是可连接的, 否则便认为这个主机是不可达的.

为了了解 golang 的网络编程, 我用 go 实现了一个 ping 命令, 本文会介绍如何实现 ping 命令.

链接:https://juejin.cn/post/6844903574833479688
代码

 

package main import ( "bytes" "encoding/binary" "fmt" "net" "os" "time" ) type ICMP struct { Type uint8 Code uint8 CheckSum uint16 Identifier uint16 SequenceNum uint16 } func usage() { msg := ` Need to run as root! Usage: goping host Example: ./goping www.baidu.com` fmt.Println(msg) os.Exit(0) } func getICMP(seq uint16) ICMP { icmp := ICMP{ Type: 8, Code: 0, CheckSum: 0, Identifier: 0, SequenceNum: seq, } var buffer bytes.Buffer binary.Write(&buffer, binary.BigEndian, icmp) icmp.CheckSum = CheckSum(buffer.Bytes()) buffer.Reset() return icmp } func sendICMPRequest(icmp ICMP, destAddr *net.IPAddr) error { conn, err := net.DialIP("ip4:icmp", nil, destAddr) if err != nil { fmt.Printf("Fail to connect to remote host: %s\n", err) return err } defer conn.Close() var buffer bytes.Buffer binary.Write(&buffer, binary.BigEndian, icmp) if _, err := conn.Write(buffer.Bytes()); err != nil { return err } tStart := time.Now() conn.SetReadDeadline((time.Now().Add(time.Second * 2))) recv := make([]byte, 1024) receiveCnt, err := conn.Read(recv) if err != nil { return err } tEnd := time.Now() duration := tEnd.Sub(tStart).Nanoseconds() / 1e6 fmt.Printf("%d bytes from %s: seq=%d time=%dms\n", receiveCnt, destAddr.String(), icmp.SequenceNum, duration) return err } func CheckSum(data []byte) uint16 { var ( sum uint32 length int = len(data) index int ) for length > 1 { sum += uint32(data[index])<<8 + uint32(data[index+1]) index += 2 length -= 2 } if length > 0 { sum += uint32(data[index]) } sum += (sum >> 16) return uint16(^sum) } func main() { if len(os.Args) < 2 { usage() } host := os.Args[1] raddr, err := net.ResolveIPAddr("ip", host) if err != nil { fmt.Printf("Fail to resolve %s, %s\n", host, err) return } fmt.Printf("Ping %s (%s):\n\n", raddr.String(), host) for i := 1; i < 6; i++ { if err = sendICMPRequest(getICMP(uint16(i)), raddr); err != nil { fmt.Printf("Error: %s\n", err) } time.Sleep(2 * time.Second) } }
 
 

package main
import (
"bytes"
"encoding/binary"
"fmt"
"net"
"os"
"time"
)
type ICMP struct {
Type uint8
Code uint8
CheckSum uint16
Identifier uint16
SequenceNum uint16
}
func usage() {
msg := `
Need to run as root!
Usage:
goping host
Example: ./goping www.baidu.com`
fmt.Println(msg)
os.Exit(0)
}
func getICMP(seq uint16) ICMP {
icmp := ICMP{
Type: 8,
Code: 0,
CheckSum: 0,
Identifier: 0,
SequenceNum: seq,
}
var buffer bytes.Buffer
binary.Write(&buffer, binary.BigEndian, icmp)
icmp.CheckSum = CheckSum(buffer.Bytes())
buffer.Reset()
return icmp
}
func sendICMPRequest(icmp ICMP, destAddr *net.IPAddr) error {
conn, err := net.DialIP("ip4:icmp", nil, destAddr)
if err != nil {
fmt.Printf("Fail to connect to remote host: %s\n", err)
return err
}
defer conn.Close()
var buffer bytes.Buffer
binary.Write(&buffer, binary.BigEndian, icmp)
if _, err := conn.Write(buffer.Bytes()); err != nil {
return err
}
tStart := time.Now()
conn.SetReadDeadline((time.Now().Add(time.Second * 2)))
recv := make([]byte, 1024)
receiveCnt, err := conn.Read(recv)
if err != nil {
return err
}
tEnd := time.Now()
duration := tEnd.Sub(tStart).Nanoseconds() / 1e6
fmt.Printf("%d bytes from %s: seq=%d time=%dms\n", receiveCnt, destAddr.String(), icmp.SequenceNum, duration)
return err
}
func CheckSum(data []byte) uint16 {
var (
sum uint32
length int = len(data)
index int
)
for length > 1 {
sum += uint32(data[index])<<8 + uint32(data[index+1])
index += 2
length -= 2
}
if length > 0 {
sum += uint32(data[index])
}
sum += (sum >> 16)
return uint16(^sum)
}
func main() {
if len(os.Args) < 2 {
usage()
}
host := os.Args[1]
raddr, err := net.ResolveIPAddr("ip", host)
if err != nil {
fmt.Printf("Fail to resolve %s, %s\n", host, err)
return
}
fmt.Printf("Ping %s (%s):\n\n", raddr.String(), host)
for i := 1; i < 6; i++ {
if err = sendICMPRequest(getICMP(uint16(i)), raddr); err != nil {
fmt.Printf("Error: %s\n", err)
}
time.Sleep(2 * time.Second)
}
}


链接:https://juejin.cn/post/6844903574833479688

 

使用 ip4:icmp 实现

即使我们想实现privileged ping,我们也不需要直接使用raw socket,还是使用icmp包。

在这种场景下,我们的network需要是ip4:icmp,能够发送ICMP包,而不是上面的udp4

package main
 
import (
"fmt"
"log"
"net"
"os"
"time"
 
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
)
 
const (
protocolICMP = 1
)
 
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "usage: %s host\n", os.Args[0])
os.Exit(1)
}
host := os.Args[1]
 
// 解析目标主机的 IP 地址
dst, err := net.ResolveIPAddr("ip", host)
if err != nil {
log.Fatal(err)
}
 
// 创建 ICMP 连接
conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
 
// 构造 ICMP 报文
msg := &icmp.Message{
Type: ipv4.ICMPTypeEcho,
Code: 0,
Body: &icmp.Echo{
ID: os.Getpid() & 0xffff,
Seq: 1,
Data: []byte("Hello, are you there!"),
},
}
msgBytes, err := msg.Marshal(nil)
if err != nil {
log.Fatal(err)
}
 
// 发送 ICMP 报文
start := time.Now()
_, err = conn.WriteTo(msgBytes, dst)
if err != nil {
log.Fatal(err)
}
 
// 接收 ICMP 报文
reply := make([]byte, 1500)
for i := 0; i < 3; i++ {
err = conn.SetReadDeadline(time.Now().Add(5 * time.Second))
if err != nil {
log.Fatal(err)
}
n, peer, err := conn.ReadFrom(reply)
if err != nil {
log.Fatal(err)
}
duration := time.Since(start)
 
// 解析 ICMP 报文
msg, err = icmp.ParseMessage(protocolICMP, reply[:n])
if err != nil {
log.Fatal(err)
}
 
// 打印结果
switch msg.Type {
case ipv4.ICMPTypeEchoReply:
echoReply, ok := msg.Body.(*icmp.Echo)
if !ok {
log.Fatal("invalid ICMP Echo Reply message")
return
}
if peer.String() == host && echoReply.ID == os.Getpid()&0xffff && echoReply.Seq == 1 {
fmt.Printf("reply from %s: seq=%d time=%v\n", dst.String(), msg.Body.(*icmp.Echo).Seq, duration)
return
}
default:
fmt.Printf("unexpected ICMP message type: %v\n", msg.Type)
}
}
}

 

//  https://colobu.com/2023/04/26/write-the-ping-tool-in-Go/

使用Go实现ping工具

ping是一个网络工具,它被广泛地用于测试网络连接的质量和稳定性。当我们想知道我们的电脑是否能够与其他设备或服务器进行通信时,ping就是我们最好的朋友。当我们想侦测网络之间的连通性和网络质量的时候,也常常使用ping工具测量,因为它是操作系统常带的一个网络诊断工具,小而强大。

ping最初是由Mike Muuss在1983年为Unix系统开发的。它的名字是来自于海军潜艇的声纳系统,声纳系统通过发送一个声波并测量其返回时间来确定目标的位置。Ping的工作原理类似,它发送一个小数据包到目标设备,然后等待该设备返回一个响应,以测量其响应时间和延迟。

当我们使用Ping测试网络连接时,它能够告诉我们两个重要的指标:延迟和丢包率。延迟是指从发送ping请求到接收到响应所需的时间,通常以毫秒为单位计算。丢包率则是指在ping请求和响应之间丢失的数据包的百分比。如果丢包率过高,说明网络连接可能存在问题,导致数据传输不稳定或者甚至无法连接。

除了基本的ping命令外,还有许多其他ping命令和选项可供使用。例如,可以使用“-c”选项指定发送ping请求的次数,使用“-i”选项指定Ping请求之间的时间间隔。此外,还可以使用“-s”选项指定发送ping请求的数据包大小。

尽管ping是一个非常有用的工具,但它也有一些限制。ping测试的结果可能会受到许多因素的影响,例如网络拥塞、防火墙、路由器丢弃等等。此外,一些设备或服务器可能已禁用对ping请求的响应,因此无法获得ping测试的结果。

尽管它有一些限制,但它仍然是网络管理员和用户必备的工具之一。

实现原理

ping工具是基于rfc 792 (ICMP协议)来实现的。它是一份名为“Internet控制消息协议(ICMP)规范”的文件,由Jon Postel和J. Reynolds在1981年9月发布。该文档定义了ICMP协议,该协议是TCP/IP网络协议套件中的一个重要组成部分。

ICMP协议是一种网络层协议,用于传输与网络控制和错误处理相关的消息。该协议通常与IP协议一起使用,用于在Internet上交换信息。RFC 792详细介绍了ICMP协议中的不同消息类型及其用途。ping就是利用发送一个Echo请求得到一个Echo Reply实现的。

 

 

 

参考:

https://juejin.cn/post/6844903574833479688

https://colobu.com/2023/04/26/write-the-ping-tool-in-Go/