linux下ping程序的代码实现

来由

这是一个作业, 目前涉及到我的知识盲区, 正在持续探索, 因此记录。

ping是什么

拿实际生活来说, 玩电脑时, 我们可以用ping baidu.com来探测自己的电脑是否联网。PING(Packet InterNet Grouper)是用来查看两台电脑是否联通的, 并统计了往返延迟(RTT, 即Round-Trip Time), 下面是一个ping的例子:

1
2
3
4
5
6
7
8
9
> ping baidu.com
PING baidu.com (110.242.68.66) 56(84) bytes of data.
64 bytes from 110.242.68.66 (110.242.68.66): icmp_seq=1 ttl=50 time=40.2 ms
64 bytes from 110.242.68.66 (110.242.68.66): icmp_seq=2 ttl=50 time=41.1 ms
64 bytes from 110.242.68.66 (110.242.68.66): icmp_seq=3 ttl=50 time=40.1 ms
^C
--- baidu.com ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 40.108/40.478/41.079/0.428 ms

从技术角度来说, ping是借助ICMP协议实现的。ICMP协议是一个差错报告协议, 可以用来检验机器间网络连通情况。ping本身是依赖ICMP协议实现, 要了解ICMP协议, 首先先了解IP协议。

除了ping, 利用ICMP协议还能做traceroute程序, 待会探讨。

ip协议报文

下面是ip报文的结构:

ip报文

  • Version(4位): 版本, 0100代表IPv4, 0110代表IPv6。

  • Length(4位): 标识首部长度, 单位为4字节, 因此首部长度最大为60字节。需要知道, 当首部长度不为4字节的整数倍的时候, 需要利用最后的填充字段进行填充。

  • Service type(8位): 服务类型, 在标准中虽然定义了这个字段, 但是实际上一直没有被使用。TODO: 所以它在现实生活中有意义吗?

  • Packet Length(16位): 标识IP报文的总长度(包括首部header和数据payload之和), 单位字节, 最大为65535字节

  • Identification(16位): IP报文的唯一标识, 由一个计数器维持。每产生一个IP报文, 计数器就+1。此IP报文的Identification就是这个计数器的当前值。

在我们的程序中, 我们的IP报文可以封装成最大65335字节包, 然而IP包的传输是依赖以太网协议的。不同设备网卡可传播的以太帧的最大数据负载是有限的(这里以MTU代表, 具体来说一个网卡设备的MTU多数为1500, 但是也有很多不是1500, 不同地区还有较大的区别), 它远小于65535字节。所以要传递一个完整的IP报文, 需要将IP报文进行分片传输。

在我们的程序中, 我们可以封装一个65535字节的IP包, 然后借助操作系统提供的接口发出。然而, 我们的网卡的MTU为1500, 此时, 网卡就会对IP包进行一个分片, 然后发出更底层的以太帧。在目标计算机中, 网卡收到以太帧后, 网卡会校验以太帧(校验MAC以及一些检查序列), 如果经过校验, 网卡会发起硬件中断, 内核就会就会根据Identification对分片进行合并。具有相同Identification的IP分片被组装成一个IP包。

那么如何知道一个IP包有多少个分片呢? 通过下面的字段可以说明。

  • Flag(3位): 其中第一位没用, 只有后面两个位有用。DF(Don’t Fragment)标识不进行分片, MF(More Fragment)标识还有更多分片。

需要知道的是, 网卡要将一个较大的IP包发出去, 需要将它分解成很多小的以太帧。因此这些分片信息(Flag + Fragment Offset)告知目标主机哪些以太帧是IP包的分片, 哪些以太帧自身的数据负载本身就是IP报文本身。

比如我们要发送一个很小的ICMP数据报文, 一个以太帧就能承载时, 这个y的数据负载中包含的IP报文有以下特征: DF被设置为1, MF此时没有意义, Fragment Offset此时没有意义。

  • Fragment Offset(13位): 分片偏移, 标识IP报文分片在IP报文中的相对位置。以8字节为偏移单位。除了最后一个分片, 其他分片的偏移值都是8字节(64位)的整数倍。

  • Time To Live(8位): 最大经过的路由器数目。

在实际网络中, 一个IP包要发送到目的地, 要经过很多路由器。每次路由器发IP包分片时, 都会将TTL值减1, 如果TTL减小到0, 则丢弃。而对于ICMP包, 此时路由器将响应特定的ICMP响应包。TTL最大值为255, 若设置为1, 那么只能在本局域网中传送。

那么TTL能达到什么目的呢? 它可以防止无法到达目的地的数据包在网络中无限制地传递, 形成”不死包”。

此外traceroute程序利用了TTL, 来探查IP包传播到目标主机的路径。具体的做法就是设置TTL为1, 2, 3…依次发包, 直到收到目标服务器的ICMP reply。

  • Transport(8位): 标识IP上层的协议类型, TCP为6, UDP为17, ICMP V4为1, ICMP V6为58

  • Header Checksum: 头部的检验。有一定的算法, 具体怎么算这里不展开。

  • Source IP Address(32位): 源地址

  • Destination IP Address(32位): 目标地址

  • Options: TODO: 这个字段是干嘛的? 在实际生活中有用吗?

  • Padding: 对齐, 上面提到了, 要求头部进行4字节的对齐。

  • Data, 又叫payload(长度不固定): TCP, ICMP, UDP的数据。

不同的数据包如何进入用户态程序?

linux下可以创建原始套接字, 它可以捕获到所有的IP包。此外我们还能创建”更上层协议”的socket, 比如TCP, UDP和ICMP。不仅如此, 更下层的以太帧也能被捕获。那么数据是怎么样通过socket递交给用户态的? 下面是一些步骤。

  1. 网卡对该以太帧进行硬件过滤, 如果mac不是本机或者校验出错则丢掉。
  2. 向用户层递交数据链路层以太帧, 此时PF_PACKET, SOCK_RAW的套接字会捕获到以太帧。
  3. 然后进入IP层过滤, 丢弃非本机IP或丢弃校验失败的IP数据包。
  4. 向用户层递交网络层数据包(IP, TCP和UDP)。

如果要创建一个收发IP报文的原始套接字, 如下:

1
int sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);

上面的代码需要root, 这种套接字会得到所有种类的IP报文, 其中IP报文的Header Checksum已经经过校验(在内核层面, 没有经过校验的IP报文已经被丢弃, 用户态是不能拿到的)。发送数据时, 默认不用自己添加IP报文头部, 内核自动添加。如果需要自己构建报文头部(其实这是没有必要的), 可以用下面的代码修改配置:

1
2
3
4
int on = 1;
if (setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) < 0) {
printf("Set IP_HDRINCL failed\n");
}

如果要使用更上层的协议比如ICMP, 可以用这种套接字:

1
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);

这种套接字和TCP, UDP同级, 不需要root权限。通过这种套接字拿到的ICMP包已经经过了checksum校验, 写入的时候也不需要自己算checksum了, 内核自己就会搞定。

如果要接收网卡传来的以太帧, 可以用更下层的协议:

1
socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))

这种套接字肯定也需要root, ETH_P_ALL代表接收发往本机mac的所有类型(IP, ARP, RARP)的数据帧, 如果只需要IP的数据帧, 那么ETH_P_IP很合适。用这种套接字可以做wireshark那样的抓包软件。用它收到的以太帧经过了CRC校验(这很有可能是网卡代劳的), 但是里面承载的数据, 比如IP包的Checksum没有经过任何校验。

要详细理解, 参考这篇文章。

ICMP协议

ICMP是建立在IP协议上的, 在发送ICMP包时, IP报文的Transport被设置为1(表示ICMP V4)。payload为ICMP包的数据。

下面是我从别人博客上扣下来的图, 结构如下:

ICMP包的结构

  • 类型type(8位)

  • 代码code(8位)

用type和code来标识ICMP包的类型: 比type=8, code=0代表一个ICMP请求包。type=0, code=0代表一个ICMP应答包。

全部的包类型被定义如下:

全部的定义

  • 还有4个字节取决于ICMP包的类型

对于ICMP Request, 需要16位的Identifier和16位的Sequence Number。对方收到ICMP请求包后会回应ICMP应答包, 原封不动地带上这两个字段。

其中前者是用来做身份校验的, 想想, 你的电脑主机上有很多个程序都可以发ICMP包。这时就需要用Identifier来做校验了, 判断应答包是否是给自己的。Identifier可以用随机数生成, 也可以取进程ID(which is preferred, 因为很多程序都是这样做区分的, 在极其巧合的情况下生成随机数可能重复, 而取进程ID不会有重复的问题)。

其中Sequence Number就是一个ICMP应答的代号, 看之前ping的一条输出:

1
64 bytes from 110.242.68.66 (110.242.68.66): icmp_seq=1 ttl=50 time=40.8 ms

icmp_seq就是指的这个Sequence Number。

  • 校验和(16位): 需要专门的校验和的算法, 待会代理里面体现

  • ICMP数据部分(可变的长度)

那么IP包什么时候会丢, 上层应用是怎么解决数据包丢失的问题的?

在linux中, root用户可以创建收发IP包的套接字。需要知道的是, IP包可能是很大的, 主机1向主机2发送一个很大的IP包, 此时将由网卡发出多个以太帧。主机2不一定能尽数接收到所有以太帧, 这是因为物理环境错综复杂, 这时一个以太帧出问题(丢包或者不通过校验), 整个IP包就无了。

看起来IP包是很容易丢的, 但是不要小看网络的稳定性, 具有优良物理效应的材料比如光纤没那么容易出问题。此外在更上面的层级, 比如tcp可以包容丢包的问题, 它通过各种确认策略来保证包被完整收到, 在内网环境中能可靠的传递数据。

实现ping程序发的ICMP包包含了什么内容? 对端响应什么呢?

ping程序发送的ICMP配置如下: type=8, code=0, 数据部分=当时的时间数据。请注意type=8, code=0是一个Echo Request, 它就是用来做回显信息的, 数据部分放时间, 可以用来统计RTT

对端的type=0, code=0, 代表这是一个Echo Reply, 会原封不动的带上之前的Sequence Number, Identifier, 以及数据负载。

在linux下如何实现ping的方法?

方法1: 前面提到, root用户可以创建收发IP包的原始套接字。我们可以自己拿取IP包, 然后分析包结构并过滤出ICMP包。创建这样的套接字的代码如下:

1
2
// 这种socket将会接收所有的IP套接字
int sock_id = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);

需要注意的是, 我们需要自己校验ICMP包的checksum, 我认为不通过校验的ICMP包都应该无视, ping程序应该视这样的情况为丢包。

方法2: 此外linux还特别提供了非特权级的ICMP socket, 非root用户就能使用。下面是使用它的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip_icmp.h>
#include <arpa/inet.h>
#include <sys/select.h>

//note, to allow root to use icmp sockets, run:
//sysctl -w net.ipv4.ping_group_range="0 0"

void ping_it(struct in_addr *dst)
{
struct icmphdr icmp_hdr;
struct sockaddr_in addr;
int sequence = 0;
int sock = socket(AF_INET,SOCK_DGRAM,IPPROTO_ICMP);
if (sock < 0) {
perror("socket");
return ;
}

memset(&addr, 0, sizeof addr);
addr.sin_family = AF_INET;
addr.sin_addr = *dst;

memset(&icmp_hdr, 0, sizeof icmp_hdr);
icmp_hdr.type = ICMP_ECHO;
icmp_hdr.un.echo.id = 1234;//arbitrary id

for (;;) {
unsigned char data[2048];
int rc;
struct timeval timeout = {3, 0}; //wait max 3 seconds for a reply
fd_set read_set;
socklen_t slen;
struct icmphdr rcv_hdr;

icmp_hdr.un.echo.sequence = sequence++;
memcpy(data, &icmp_hdr, sizeof icmp_hdr);
memcpy(data + sizeof icmp_hdr, "hello", 5); //icmp payload
rc = sendto(sock, data, sizeof icmp_hdr + 5,
0, (struct sockaddr*)&addr, sizeof addr);
if (rc <= 0) {
perror("Sendto");
break;
}
puts("Sent ICMP\n");

memset(&read_set, 0, sizeof read_set);
FD_SET(sock, &read_set);

//wait for a reply with a timeout
rc = select(sock + 1, &read_set, NULL, NULL, &timeout);
if (rc == 0) {
puts("Got no reply\n");
continue;
} else if (rc < 0) {
perror("Select");
break;
}

//we don't care about the sender address in this example..
slen = 0;
rc = recvfrom(sock, data, sizeof data, 0, NULL, &slen);
if (rc <= 0) {
perror("recvfrom");
break;
} else if (rc < sizeof rcv_hdr) {
printf("Error, got short ICMP packet, %d bytes\n", rc);
break;
}
memcpy(&rcv_hdr, data, sizeof rcv_hdr);
if (rcv_hdr.type == ICMP_ECHOREPLY) {
printf("ICMP Reply, id=0x%x, sequence = 0x%x\n",
icmp_hdr.un.echo.id, icmp_hdr.un.echo.sequence);
} else {
printf("Got ICMP packet with type 0x%x ?!?\n", rcv_hdr.type);
}
}
}

int main(int argc, char *argv[])
{
if (argc != 2) {
printf("usage: %s destination_ip\n", argv[0]);
return 1;
}
struct in_addr dst;

if (inet_aton(argv[1], &dst) == 0) {

perror("inet_aton");
printf("%s isn't a valid IP address\n", argv[1]);
return 1;
}

ping_it(&dst);
return 0;
}

如果需要处理ICMP包, 尽量用它, 因为它提供了checksum的校验, 收的ICMP包已经进行checksum校验, 发送时内核也会自己填写checksum, 操心更少。

用go实现一个ping

go提供的golang.org/x/net/icmp用是使用的raw socket实现的, 用它代码很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
package main

import (
"bytes"
"encoding/binary"
"fmt"
"log"
"math/rand"
"net"
"os"
"syscall"
"time"

"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
)

const (
TimeSliceLength = 8
ProtocolICMP = 1
ProtocolIPv6ICMP = 58
)

func timeToBytes(t time.Time) []byte {
nsec := t.UnixNano()
b := make([]byte, 8)
for i := uint8(0); i < 8; i++ {
b[i] = byte((nsec >> ((7 - i) * 8)) & 0xff)
}
return b
}

func bytesToTime(b []byte) time.Time {
var nsec int64
for i := uint8(0); i < 8; i++ {
nsec += int64(b[i]) << ((7 - i) * 8)
}
return time.Unix(nsec/1000000000, nsec%1000000000)
}

func isIPv4(ip net.IP) bool {
return len(ip.To4()) == net.IPv4len
}

func isIPv6(ip net.IP) bool {
return len(ip) == net.IPv6len
}

func checksum(b []byte) uint16 {
csumcv := len(b) - 1 // checksum coverage
s := uint32(0)
for i := 0; i < csumcv; i += 2 {
s += uint32(b[i+1])<<8 | uint32(b[i])
}
if csumcv&1 == 0 {
s += uint32(b[csumcv])
}
s = s>>16 + s&0xffff
s = s + s>>16
return ^uint16(s)
}

func main() {
rand.Seed(time.Now().UnixNano())

if len(os.Args) != 2 {
fmt.Printf("usage: %v <target IP>\n", os.Args[0])
os.Exit(1)
}

ip := net.ParseIP(os.Args[1])
if !isIPv4(ip) && !isIPv6(ip) {
addrs, err := net.LookupHost(os.Args[1])
if err != nil {
fmt.Printf("name or service not known: %v\n", err)
os.Exit(1)
}
ip = net.ParseIP(addrs[0])
}

var proto int
var conn *icmp.PacketConn
var err error
switch {
case isIPv4(ip):
conn, err = icmp.ListenPacket("ip4:icmp", "0.0.0.0")
proto = ProtocolICMP
case isIPv6(ip):
conn, err = icmp.ListenPacket("ip6:icmp", "0.0.0.0")
proto = ProtocolIPv6ICMP
default:
log.Fatalln("unreachable")
}
if err != nil {
fmt.Printf("failed to ListenPacket: %v\n", err)
os.Exit(1)
}

lastSeq := 0
for {
lastID := rand.Intn(0xffff)
lastSeq++
bytes_, err := (&icmp.Message{
Type: ipv4.ICMPTypeEcho,
Code: 0,
Body: &icmp.Echo{
ID: lastID,
Seq: lastSeq,
Data: timeToBytes(time.Now()),
},
}).Marshal(nil)
if err != nil {
log.Fatalf("unexpected: %v\n", err)
}
retry:
if _, err := conn.WriteTo(bytes_, &net.IPAddr{IP: ip}); err != nil {
if neterr, ok := err.(*net.OpError); ok {
if neterr.Err == syscall.ENOBUFS {
goto retry
}
}
log.Fatalf("unexpected: %v\n", err)
}

// 收包
b := make([]byte, 2048)
for {
n, ra, err := conn.ReadFrom(b)
if err != nil {
log.Fatalf("unexpected: %v\n", err)
}

raAddr := ra.(*net.IPAddr)
var protoResp int
if isIPv4(raAddr.IP) {
protoResp = ProtocolICMP
} else {
protoResp = ProtocolIPv6ICMP
}

// 不匹配的协议, 直接跳过
if proto != protoResp {
continue
}

// 解包错误, 视为丢包
m, err := icmp.ParseMessage(protoResp, b[:n])
if err != nil {
fmt.Printf("packet loss\n")
continue
}

// 校验和不通过, 视为丢包
b[2] = 0
b[3] = 0
var buffer bytes.Buffer
buffer.Reset()
binary.Write(&buffer, binary.BigEndian, b[1:n])
if m.Checksum != int(checksum(buffer.Bytes())) {
fmt.Printf("packet loss: %v %v\n", m.Checksum, checksum(buffer.Bytes()))
continue
}

if m.Type != ipv4.ICMPTypeEchoReply && m.Type != ipv6.ICMPTypeEchoReply {
continue
}

echoReply := (m.Body).(*icmp.Echo)
if echoReply.ID == lastID && echoReply.Seq == lastSeq {
rtt := time.Since(bytesToTime(echoReply.Data[:TimeSliceLength]))
fmt.Printf("icmp_seq=%v time=%v\n", lastSeq, rtt)
break
}
}
time.Sleep(time.Second * 1)
}
}

注意这个程序需要root权限才能运行, 运行结果展示:

1
2
3
4
5
6
7
8
9
10
> sudo go run main.go
icmp_seq=1 time=41.126416ms
icmp_seq=2 time=41.889806ms
icmp_seq=3 time=39.803255ms
icmp_seq=4 time=39.684948ms
icmp_seq=5 time=42.727745ms
icmp_seq=6 time=40.492281ms
icmp_seq=7 time=40.031703ms
^Csignal: interrupt
>

需要注意的是, golang的icmp实现是通过raw socket实现的。这里吐槽下icmp包实现不是很好, 上面的Marshal会自动生成checksum, 然而ParseMessage不会校验checksum, 我们需要自己进行一个校验(我的理解是校验和不通过那么视为丢包, 这里要自己校验一下)。

More?

我们的程序没有TTL, 现在我们来讨论一下TTL。

当我们发送包的时候, 需要给IP包分片带上TTL, 它代表能经过路由器的最大数目, 它可以尽可能的大, 在我的操作系统中, 发一个TCP包, TTL是64, ICMP包也是64, UDP包为128。对端收到包后ICMP的Echo Request后发Echo Reply。之后我们的ping程序收到Echo Reply将得到TTL的剩余值, 那就是ping程序的tty打印。

那么按上面的说法, TTL剩余值越大, 经过的路由器越少, 延迟理应越低。

可惜的是, 我们的icmp包, 没有支持TTL的访问, 要访问TTL, 需要拿到c的接口, 用ipv4.NewPacketConn拿到。可以参考下面的demo, 实现了go语言的traceroute, 代码是我从互联网上copy的, 能看清原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
package main

import (
"fmt"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"log"
"net"
"os"
"time"
)

func main() {
// Tracing an IP packet route to www.baidu.com.

const host = "baidu.com"
ips, err := net.LookupIP(host)
if err != nil {
log.Fatal(err)
}
var dst net.IPAddr
for _, ip := range ips {
if ip.To4() != nil {
dst.IP = ip
fmt.Printf("using %v for tracing an IP packet route to %s\n", dst.IP, host)
break
}
}
if dst.IP == nil {
log.Fatal("no A record found")
}

c, err := net.ListenPacket("ip4:1", "0.0.0.0") // ICMP for IPv4
if err != nil {
log.Fatal(err)
}
defer c.Close()
p := ipv4.NewPacketConn(c)

if err := p.SetControlMessage(ipv4.FlagTTL|ipv4.FlagSrc|ipv4.FlagDst|ipv4.FlagInterface, true); err != nil {
log.Fatal(err)
}
wm := icmp.Message{
Type: ipv4.ICMPTypeEcho, Code: 0,
Body: &icmp.Echo{
ID: os.Getpid() & 0xffff,
Data: []byte("HELLO-R-U-THERE"),
},
}

rb := make([]byte, 1500)
for i := 1; i <= 64; i++ { // up to 64 hops
wm.Body.(*icmp.Echo).Seq = i
wb, err := wm.Marshal(nil)
if err != nil {
log.Fatal(err)
}
if err := p.SetTTL(i); err != nil {
log.Fatal(err)
}

// In the real world usually there are several
// multiple traffic-engineered paths for each hop.
// You may need to probe a few times to each hop.
begin := time.Now()
if _, err := p.WriteTo(wb, nil, &dst); err != nil {
log.Fatal(err)
}
if err := p.SetReadDeadline(time.Now().Add(3 * time.Second)); err != nil {
log.Fatal(err)
}
n, cm, peer, err := p.ReadFrom(rb)
if err != nil {
if err, ok := err.(net.Error); ok && err.Timeout() {
fmt.Printf("%v\t*\n", i)
continue
}
log.Fatal(err)
}
rm, err := icmp.ParseMessage(1, rb[:n])
if err != nil {
log.Fatal(err)
}
rtt := time.Since(begin)

// In the real world you need to determine whether the
// received message is yours using ControlMessage.Src,
// ControlMessage.Dst, icmp.Echo.ID and icmp.Echo.Seq.
switch rm.Type {
case ipv4.ICMPTypeTimeExceeded:
names, _ := net.LookupAddr(peer.String())
fmt.Printf("%d\t%v %+v %v\n\t%+v\n", i, peer, names, rtt, cm)
case ipv4.ICMPTypeEchoReply:
names, _ := net.LookupAddr(peer.String())
fmt.Printf("%d\t%v %+v %v\n\t%+v\n", i, peer, names, rtt, cm)
return
default:
log.Printf("unknown ICMP message: %+v\n", rm)
}
}
}

我要造轮子

为了解决go语言收发ICMP需要root的痛点, 我打算封装一个socket ICMP协议的包。先挖个坑, 以后再说。