-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
431 lines (372 loc) · 11 KB
/
main.go
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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
package main
import (
"flag"
"fmt"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
"net"
"os"
"os/signal"
"strconv"
"strings"
"sync"
"time"
)
const icmpv4ProtocolNum = 1
const icmpv6ProtocolNum = 58
// ID is sent along with Echo requests and responses, by using the PID we know
// that an Echo response is intended for this ping process and not another
// running simulataneously
var icmpEchoID = uint16(os.Getpid() & 0xFFFF)
// The following are dependent on whether we are using ICMP or ICMPv6
var icmpProtocolNum int
var icmpTypeEchoRequest icmp.Type
var icmpTypeEchoReply icmp.Type
var icmpTypeTimeExceeded icmp.Type
// Results from CLI flags
var ttl int
var timeBetweenRequests time.Duration
var requestCount countValue
var bellOnResponse bool
var ipv4Only bool
var ipv6Only bool
// flag.IntVar requires a default value, but for -count the default is off
// countValue is a workaround, implements the flag.Value interface
type countValue struct {
count int
enabled bool
}
func (c *countValue) String() string {
if c.enabled {
return string(c.count)
}
return "off"
}
func (c *countValue) Set(s string) (err error) {
c.count, err = strconv.Atoi(s)
if err != nil && c.count <= 0 {
return fmt.Errorf("count must be > 0")
}
c.enabled = err == nil
return
}
func init() {
flag.IntVar(&ttl, "ttl", 64, "TTL `value` for ICMP echo requests (from 1 to 255)")
flag.DurationVar(&timeBetweenRequests, "wait", time.Second, "Time to wait between sending requests (>= 0.1s)")
flag.Var(&requestCount, "count", "Stop after sending `n` requests, will wait for response or timeout (> 0) (default off)")
flag.BoolVar(&bellOnResponse, "beepboop", false, "Beep (using the bell character) when an ICMP echo reply is received")
flag.BoolVar(&ipv4Only, "4", false, "Use IPv4 only (mututally exclusive with -6)")
flag.BoolVar(&ipv6Only, "6", false, "Use IPv6 only (mututally exclusive with -4)")
flag.CommandLine.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] host\n\n", os.Args[0])
fmt.Fprintln(flag.CommandLine.Output(), "Options:")
flag.PrintDefaults()
}
}
func handleFatalError(err error) {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
func handleNonfatalError(err error) {
fmt.Fprintln(os.Stderr, err)
}
// Validate optional arguments and return host positional argument.
func args() (host string) {
flag.Parse()
if (ttl < 1 || 255 < ttl) ||
(flag.NArg() < 1) ||
(timeBetweenRequests.Milliseconds() < 100) ||
(ipv4Only && ipv6Only) {
flag.CommandLine.Usage()
os.Exit(2)
}
return flag.Arg(0)
}
// Get an IP address that the host resolves to.
// Prioritizes IPv4 address over IPv6, constrained by ipv4Only and ipv6Only.
// Returns the IP, if the IP is IPv4, and potentially an error.
func ipFromHost(host string) (net.IP, bool, error) {
ips, err := net.LookupIP(host)
if err != nil {
return nil, false, err
}
var firstIPv6 net.IP
for _, ip := range ips {
if !ipv6Only && ip.To4() != nil {
return ip, true, nil
}
if !ipv4Only && firstIPv6 == nil && ip.To4() == nil {
firstIPv6 = ip
}
}
if firstIPv6 != nil {
return firstIPv6, false, nil
}
ipString := "IP"
if ipv4Only {
ipString = "IPv4"
} else if ipv6Only {
ipString = "IPv6"
}
return nil, false, fmt.Errorf("No %s addresses found for %s.", ipString, host)
}
func isDueToClosedPacketConn(err error) bool {
// Doesn't seem to be a better way.
// golang.org/src/net/error_test.go even has tests to ensure that errors
// due to a closed network connection nest include this string
return strings.Contains(err.Error(), "use of closed network connection")
}
// Send ICMP Echo requests every `timeBetweenRequests` until either the done chan
// is closed or if using -count, `requestCount.count` requests have been sent.
func send(conn *icmp.PacketConn, addr net.Addr, pingStatistics *PingStatistics,
wg *sync.WaitGroup, done <-chan struct{}) {
marshalledEchoReqIPv4 := func(seq uint16) ([]byte, error) {
message := icmp.Message{
Type: icmpTypeEchoRequest, Code: 0,
Body: &icmp.Echo{
ID: int(icmpEchoID), Seq: int(seq),
Data: []byte("This is 56 bytes!! This is 56 bytes!! This is 56 bytes!!"),
},
}
return message.Marshal(nil)
}
requestTimeoutPrinter := func(seq uint16) {
defer wg.Done()
timer := time.NewTimer(pingStatistics.responseTimeout())
// block until timer is done, or done chan is closed
select {
case <-done:
if !timer.Stop() {
<-timer.C
}
case <-timer.C:
if pingStatistics.isStillWaitingForResp(seq) {
fmt.Printf("Request timeout for icmp_seq %v\n", seq)
}
timer.Stop()
}
}
defer wg.Done()
ticker := time.NewTicker(timeBetweenRequests)
defer ticker.Stop()
// Loop until done chan is closed or specified number of requests have been sent (-count)
for seq, i := uint16(0), 0; !requestCount.enabled || i < requestCount.count; seq, i = seq+1, i+1 {
// Block until ticker fires or done chan closed
select {
case <-done:
return
case <-ticker.C:
request, err := marshalledEchoReqIPv4(seq)
if err != nil {
handleFatalError(err)
}
if _, err := conn.WriteTo(request, addr); err != nil {
if isDueToClosedPacketConn(err) {
continue
}
handleNonfatalError(err)
pingStatistics.requestFailed()
continue
}
pingStatistics.addRequest(seq)
// goroutine to print timeout message if necessary
wg.Add(1)
go requestTimeoutPrinter(seq)
}
}
}
// Return string with `host (ip)` where host is the result of a reverse lookup.
// If the lookup fails, returns `ip (ip)`.
func hostIPString(ip net.IP) string {
var hostName string
addrs, err := net.LookupAddr(ip.String())
if err == nil && len(addrs) > 0 {
hostName = addrs[0]
lastIndex := len(addrs[0]) - 1
if hostName[lastIndex] == '.' {
hostName = hostName[:lastIndex]
}
} else {
hostName = ip.String()
}
return fmt.Sprintf("%s (%v)", hostName, ip)
}
// Receive, print, and update statistics for Echo responses and Time Exceeded
// messages until done chan is closed.
func receive(conn *icmp.PacketConn, pingStatistics *PingStatistics,
wg *sync.WaitGroup, done <-chan struct{}) {
defer wg.Done()
buffer := make([]byte, 1500)
isIPv4 := conn.IPv4PacketConn() != nil
if !isIPv4 && conn.IPv6PacketConn() == nil {
panic("icmp.PacketConn has neither an IPv4 raw socket nor an IPv6 raw socket.")
}
for {
select {
case <-done:
return
default:
// Block until response received (or socket closed)
// Using underlying raw socket (instead of just conn.ReadFrom) to
// get TTL of Echo responses from IP header
var n, respTTL int
var addr net.Addr
var err error
if isIPv4 {
var cm *ipv4.ControlMessage
n, cm, addr, err = conn.IPv4PacketConn().ReadFrom(buffer)
if cm != nil {
respTTL = cm.TTL
}
} else {
var cm *ipv6.ControlMessage
n, cm, addr, err = conn.IPv6PacketConn().ReadFrom(buffer)
if cm != nil {
respTTL = cm.HopLimit
}
}
if err != nil {
if isDueToClosedPacketConn(err) {
continue
}
handleFatalError(err)
}
var ip net.IP = addr.(*net.UDPAddr).IP
// Parse response
response, err := icmp.ParseMessage(icmpProtocolNum, buffer[:n])
if err != nil {
handleFatalError(err)
}
if echoReply, ok := response.Body.(*icmp.Echo); ok &&
response.Type == icmpTypeEchoReply &&
echoReply.ID == int(icmpEchoID) {
rtt, err := pingStatistics.rttMilliseconds(uint16(echoReply.Seq))
if err != nil {
handleFatalError(err)
}
var optionalBellChar string
if bellOnResponse {
optionalBellChar = "\a"
}
fmt.Printf("%d bytes from %s: icmp_seq=%d ttl=%d time=%.3f ms%s\n",
n, hostIPString(ip), echoReply.Seq, respTTL, rtt, optionalBellChar)
} else if te, ok := response.Body.(*icmp.TimeExceeded); ok &&
response.Type == icmpTypeTimeExceeded {
// te.Data is IP Header + ICMP Echo Request from the Echo
// request that's TTL reached 0. Chop off the IP header and
// parse the ICMP Echo Request to check the ID and Seq number.
var headerLength int
if isIPv4 {
headerLength = int(te.Data[0]&0x0f) << 2
} else {
headerLength = 40
}
origReq, err := icmp.ParseMessage(icmpProtocolNum, te.Data[headerLength:])
if err != nil {
handleFatalError(err)
}
if origEcho, ok := origReq.Body.(*icmp.Echo); ok {
if origEcho.ID != int(icmpEchoID) {
fmt.Printf("Got time exceed from %v but bad id.\n", addr)
return
}
err = pingStatistics.requestGotTimeExceeded(uint16(origEcho.Seq))
if err != nil {
handleFatalError(err)
}
fmt.Printf("From %s icmp_seq=%d Time to live exceeded\n",
hostIPString(ip), origEcho.Seq)
}
}
}
}
}
func main() {
host := args()
ip, ipIsIPv4, err := ipFromHost(host)
if err != nil {
handleFatalError(err)
}
var addr net.Addr = &net.UDPAddr{IP: ip, Zone: ""}
// Setup socket
var conn *icmp.PacketConn
if ipIsIPv4 {
conn, err = icmp.ListenPacket("udp4", "0.0.0.0")
icmpProtocolNum = icmpv4ProtocolNum
icmpTypeEchoRequest = ipv4.ICMPTypeEcho
icmpTypeEchoReply = ipv4.ICMPTypeEchoReply
icmpTypeTimeExceeded = ipv4.ICMPTypeTimeExceeded
} else {
conn, err = icmp.ListenPacket("udp6", "::")
icmpProtocolNum = icmpv6ProtocolNum
icmpTypeEchoRequest = ipv6.ICMPTypeEchoRequest
icmpTypeEchoReply = ipv6.ICMPTypeEchoReply
icmpTypeTimeExceeded = ipv6.ICMPTypeTimeExceeded
}
if err != nil {
handleFatalError(err)
}
defer conn.Close()
// Have underlying raw socket return TTL in ControlMessage
if ipIsIPv4 {
err = conn.IPv4PacketConn().SetControlMessage(ipv4.FlagTTL, true)
} else {
err = conn.IPv6PacketConn().SetControlMessage(ipv6.FlagHopLimit, true)
}
if err != nil {
handleFatalError(err)
}
// Set TTL for requests based on CLI flag
if ipIsIPv4 {
err = conn.IPv4PacketConn().SetTTL(ttl)
if err != nil {
handleFatalError(err)
}
err = conn.IPv4PacketConn().SetMulticastTTL(ttl)
if err != nil {
handleFatalError(err)
}
} else {
err = conn.IPv6PacketConn().SetHopLimit(ttl)
if err != nil {
handleFatalError(err)
}
err = conn.IPv6PacketConn().SetMulticastHopLimit(ttl)
if err != nil {
handleFatalError(err)
}
}
fmt.Printf("PING %s (%v): 56 data bytes\n", host, ip)
done := make(chan struct{})
var pingStatistics *PingStatistics
var responseDoneChan <-chan struct{}
if requestCount.enabled {
pingStatistics, responseDoneChan = NewPingStatisticsWithDoneChan(time.Second,
requestCount.count)
} else {
pingStatistics = NewPingStatistics(time.Second)
}
var wg sync.WaitGroup
wg.Add(2)
go send(conn, addr, pingStatistics, &wg, done)
go receive(conn, pingStatistics, &wg, done)
// Let send, receive do their thing, until SIGINT is received (^C) or
// requestCount.count responses (or timeouts) are received
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, os.Interrupt)
if requestCount.enabled {
select {
case <-responseDoneChan:
case <-sigs:
}
} else {
<-sigs
}
// Tell the goroutines to stop, close the connection, wait for them to stop
close(done)
conn.Close()
wg.Wait()
fmt.Printf("\n--- %v ping statistics ---\n", host)
pingStatistics.printStatistics()
}