├── .gitignore
├── build.sh
├── dashboard.go
├── dashboardIncludes.go
├── diskPersist.go
├── linux
└── duck
├── mac
└── duck
├── main.go
├── readme.md
├── types.go
└── windows
└── duck.exe
/.gitignore:
--------------------------------------------------------------------------------
1 | *.json
2 | /duck
3 |
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | GOOS=darwin go build -o mac/duck
2 | GOOS=windows go build -o windows/duck.exe
3 | GOOS=linux go build -o linux/duck
4 |
--------------------------------------------------------------------------------
/dashboard.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "time"
7 |
8 | "github.com/gorilla/websocket"
9 | )
10 |
11 | var dashboard string = fmt.Sprintf(`
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
423 |
424 |
425 |
426 |
427 |
428 |
429 | `, jquery, boostrapCSS, bootstrapJS, bootstrapSlateCSS, highchartsJS, highchartsMoreJS, highchartsDarkUnicaThemeJS)
430 |
431 | func wsHandler(w http.ResponseWriter, req *http.Request) {
432 | ws, err := wsUpgrader.Upgrade(w, req, nil)
433 | if err != nil {
434 | elog.Print(err)
435 | return
436 | }
437 |
438 | c := &wsClient{
439 | send: make(chan []byte, maxMessageSize),
440 | ws: ws,
441 | }
442 |
443 | wsHub.register <- c
444 |
445 | go c.writePump()
446 | go c.readPump()
447 | }
448 |
449 | func historyHandler(w http.ResponseWriter, req *http.Request) {
450 | w.Header().Add("Content-Type", "application/json")
451 |
452 | resultRequestChan <- 1
453 | historyJSON := <-resultResponseChan
454 |
455 | w.Write(historyJSON)
456 | }
457 |
458 | func baseHandler(w http.ResponseWriter, req *http.Request) {
459 | w.Header().Add("Content-Type", "text/html")
460 | w.Write([]byte(dashboard))
461 | }
462 |
463 | const (
464 | writeWait = 10 * time.Second
465 | pongWait = 60 * time.Second
466 | pingPeriod = (pongWait * 9) / 10
467 | maxMessageSize = 1024 * 1024
468 | )
469 |
470 | var wsUpgrader = websocket.Upgrader{
471 | ReadBufferSize: 1024,
472 | WriteBufferSize: 1024,
473 | }
474 |
475 | type hub struct {
476 | clients map[*wsClient]bool
477 | broadcast chan string
478 | register chan *wsClient
479 | unregister chan *wsClient
480 |
481 | content string
482 | }
483 |
484 | type wsClient struct {
485 | ws *websocket.Conn
486 | send chan []byte
487 | }
488 |
489 | var wsHub = hub{
490 | broadcast: make(chan string),
491 | register: make(chan *wsClient),
492 | unregister: make(chan *wsClient),
493 | clients: make(map[*wsClient]bool),
494 | content: "",
495 | }
496 |
497 | func (h *hub) run() {
498 | for {
499 | select {
500 | case c := <-h.register:
501 | h.clients[c] = true
502 | c.send <- []byte(h.content)
503 | break
504 |
505 | case c := <-h.unregister:
506 | _, ok := h.clients[c]
507 | if ok {
508 | delete(h.clients, c)
509 | close(c.send)
510 | }
511 | break
512 |
513 | case m := <-h.broadcast:
514 | h.content = m
515 | h.broadcastMessage()
516 | break
517 | }
518 | }
519 | }
520 |
521 | func (h *hub) broadcastMessage() {
522 | for c := range h.clients {
523 | select {
524 | case c.send <- []byte(h.content):
525 | break
526 |
527 | // We can't reach the client
528 | default:
529 | close(c.send)
530 | delete(h.clients, c)
531 | }
532 | }
533 | }
534 |
535 | func (c *wsClient) readPump() {
536 | defer func() {
537 | wsHub.unregister <- c
538 | c.ws.Close()
539 | }()
540 |
541 | c.ws.SetReadLimit(maxMessageSize)
542 | c.ws.SetReadDeadline(time.Now().Add(pongWait))
543 | c.ws.SetPongHandler(func(string) error {
544 | c.ws.SetReadDeadline(time.Now().Add(pongWait))
545 | return nil
546 | })
547 |
548 | for {
549 | _, message, err := c.ws.ReadMessage()
550 | if err != nil {
551 | break
552 | }
553 |
554 | wsHub.broadcast <- string(message)
555 | }
556 | }
557 |
558 | func (c *wsClient) writePump() {
559 | ticker := time.NewTicker(pingPeriod)
560 |
561 | defer func() {
562 | ticker.Stop()
563 | c.ws.Close()
564 | }()
565 |
566 | for {
567 | select {
568 | case message, ok := <-c.send:
569 | if !ok {
570 | c.write(websocket.CloseMessage, []byte{})
571 | return
572 | }
573 | if err := c.write(websocket.TextMessage, message); err != nil {
574 | return
575 | }
576 | case <-ticker.C:
577 | if err := c.write(websocket.PingMessage, []byte{}); err != nil {
578 | return
579 | }
580 | }
581 | }
582 | }
583 |
584 | func (c *wsClient) write(mt int, message []byte) error {
585 | c.ws.SetWriteDeadline(time.Now().Add(writeWait))
586 | return c.ws.WriteMessage(mt, message)
587 | }
588 |
--------------------------------------------------------------------------------
/diskPersist.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "io/ioutil"
6 | "log"
7 | "os"
8 | "path/filepath"
9 | "time"
10 | )
11 |
12 | func saveProcessor() {
13 |
14 | saveTicker := time.NewTicker(time.Minute)
15 |
16 | for {
17 | select {
18 | case <-saveTicker.C:
19 | err := saveDataToDisk("pings", pingResults)
20 | if err != nil {
21 | log.Print(err)
22 | }
23 |
24 | pathResultsRequestChan <- 1
25 | prr := <-pathResultsResponseChan
26 |
27 | err = saveDataToDisk("paths", prr)
28 | if err != nil {
29 | log.Print(err)
30 | }
31 |
32 | pathsResult := PathsResult{
33 | Paths: prr,
34 | MessageType: "paths",
35 | }
36 |
37 | pathsJSON, err := json.Marshal(pathsResult)
38 | if err != nil {
39 | elog.Print(err)
40 | }
41 |
42 | wsHub.broadcast <- string(pathsJSON)
43 |
44 | hostResultsRequestChan <- 1
45 | hrr := <-hostResultsResponseChan
46 |
47 | err = saveDataToDisk("hosts", hrr)
48 | if err != nil {
49 | log.Print(err)
50 | }
51 |
52 | hostsResult := HostsResult{
53 | Hosts: hrr,
54 | MessageType: "hosts",
55 | }
56 |
57 | hostsJSON, err := json.Marshal(hostsResult)
58 | if err != nil {
59 | elog.Print(err)
60 | }
61 |
62 | wsHub.broadcast <- string(hostsJSON)
63 |
64 | }
65 | }
66 | }
67 |
68 | func saveDataToDisk(name string, data interface{}) error {
69 |
70 | name = name + resultFileNameSuffix
71 |
72 | log.Printf("Saving %s to disk", name)
73 |
74 | dataPath, err := filepath.Abs(name + ".json")
75 | if err != nil {
76 | return err
77 | }
78 |
79 | rf, err := os.OpenFile(dataPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0775)
80 | if err != nil {
81 | return err
82 | }
83 | defer rf.Close()
84 |
85 | dataJSON, err := json.Marshal(data)
86 | if err != nil {
87 | return err
88 | }
89 |
90 | _, err = rf.Write(dataJSON)
91 | if err != nil {
92 | return err
93 | }
94 |
95 | log.Printf("Saved %s to disk", name)
96 |
97 | return nil
98 | }
99 |
100 | func readDataFromDisk(name string, data interface{}) error {
101 |
102 | name = name + resultFileNameSuffix
103 |
104 | log.Printf("Reading %s from disk", name)
105 |
106 | dataPath, err := filepath.Abs(name + ".json")
107 | if err != nil {
108 | return err
109 | }
110 |
111 | rf, err := os.Open(dataPath)
112 | if err != nil {
113 | return err
114 | }
115 | defer rf.Close()
116 |
117 | dataJSON, err := ioutil.ReadAll(rf)
118 | if err != nil {
119 | return err
120 | }
121 |
122 | err = json.Unmarshal(dataJSON, data)
123 | if err != nil {
124 | return err
125 | }
126 |
127 | log.Printf("Read %s from disk", name)
128 |
129 | return nil
130 | }
131 |
--------------------------------------------------------------------------------
/linux/duck:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brendanporter/duck/87b520f1a9069767767199da6a20caf64c79a94c/linux/duck
--------------------------------------------------------------------------------
/mac/duck:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brendanporter/duck/87b520f1a9069767767199da6a20caf64c79a94c/mac/duck
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "log"
7 | "net"
8 | "os"
9 | "regexp"
10 | "runtime"
11 |
12 | //"strconv"
13 | "encoding/json"
14 | "math"
15 | "net/http"
16 | "strings"
17 | "time"
18 |
19 | "github.com/brendanporter/quack"
20 | "golang.org/x/net/ipv4"
21 | )
22 |
23 | const CLR_0 = "\x1b[30;1m"
24 | const CLR_R = "\x1b[31;1m"
25 | const CLR_G = "\x1b[32;1m"
26 | const CLR_Y = "\x1b[33;1m"
27 | const CLR_B = "\x1b[34;1m"
28 | const CLR_M = "\x1b[35;1m"
29 | const CLR_C = "\x1b[36;1m"
30 | const CLR_W = "\x1b[37;1m"
31 | const CLR_N = "\x1b[0m"
32 |
33 | var elog *log.Logger
34 |
35 | var pingResults map[string][]quack.PingResult
36 |
37 | var pingResultChan chan quack.PingResult
38 | var resultRequestChan chan int
39 | var resultResponseChan chan []byte
40 |
41 | var ttlTraceResultChan chan []quack.PingResult
42 | var unhealthyPingResultChan chan quack.PingResult
43 |
44 | var pathResultsChan chan *PathStats
45 | var pathResultsRequestChan chan int
46 | var pathResultsResponseChan chan map[string]*PathStats
47 |
48 | var hostResultsChan chan *quack.PingResult
49 | var hostResultsRequestChan chan int
50 | var hostResultsResponseChan chan map[string]*HostStats
51 |
52 | func resultProcessor() {
53 |
54 | paths := make(map[string]*PathStats)
55 | hosts := make(map[string]*HostStats)
56 |
57 | err := readDataFromDisk("pings", &pingResults)
58 | if err != nil {
59 | elog.Print(err)
60 | }
61 |
62 | err = readDataFromDisk("paths", &paths)
63 | if err != nil {
64 | elog.Print(err)
65 | }
66 |
67 | err = readDataFromDisk("hosts", &hosts)
68 | if err != nil {
69 | elog.Print(err)
70 | }
71 |
72 | go wsHub.run()
73 |
74 | http.HandleFunc("/history", historyHandler)
75 | http.HandleFunc("/ws", wsHandler)
76 | http.HandleFunc("/", baseHandler)
77 | server := http.Server{
78 | Addr: ":14445",
79 | }
80 |
81 | go server.ListenAndServe()
82 |
83 | go saveProcessor()
84 |
85 | for {
86 | select {
87 | case pr := <-pingResultChan:
88 |
89 | if pr.ICMPMessage.Type == ipv4.ICMPTypeDestinationUnreachable {
90 | fmt.Printf("Destination network unreachable\n")
91 | continue
92 | }
93 |
94 | var color string = CLR_W
95 | if pr.Latency < 40.0 {
96 | color = CLR_G
97 | } else if pr.Latency > 65.0 && pr.Latency < 150.0 {
98 | color = CLR_Y
99 | } else if pr.Latency > 150.0 {
100 | color = CLR_R
101 | }
102 |
103 | barCount := int((pr.Latency / 2000) * 80)
104 |
105 | if runtime.GOOS != "windows" {
106 | fmt.Printf("%d bytes from %s: icmp_seq=%d ttl=%d time=%s%.3f ms |%s|%s\n", pr.Size, pr.Peer, pr.Sequence, pr.TTL, color, pr.Latency, strings.Repeat("-", barCount), CLR_W)
107 |
108 | } else {
109 | fmt.Printf("%d bytes from %s: icmp_seq=%d ttl=%d time=%.3f ms |%s|\n", pr.Size, pr.Peer, pr.Sequence, pr.TTL, pr.Latency, strings.Repeat("-", barCount))
110 | }
111 |
112 | pingResults[pr.Target] = append(pingResults[pr.Target], pr)
113 | jsonBytes, err := json.Marshal(pr)
114 | if err != nil {
115 | elog.Print(err)
116 | } else {
117 | wsHub.broadcast <- string(jsonBytes)
118 | }
119 |
120 | case traceResult := <-ttlTraceResultChan:
121 |
122 | tr := TraceResult{
123 | Hops: traceResult,
124 | MessageType: "hops",
125 | }
126 | jsonBytes, err := json.Marshal(tr)
127 | if err != nil {
128 | elog.Print(err)
129 | } else {
130 | wsHub.broadcast <- string(jsonBytes)
131 | }
132 |
133 | case unhealthyPingResult := <-unhealthyPingResultChan:
134 |
135 | if unhealthyPingResult.Time < 100000 {
136 | break
137 | }
138 | upr := UnhealthyPingResult{
139 | Target: unhealthyPingResult.Target,
140 | Latency: unhealthyPingResult.Latency,
141 | Time: unhealthyPingResult.Time,
142 | MessageType: "unhealthyPingResult",
143 | }
144 | jsonBytes, err := json.Marshal(upr)
145 | if err != nil {
146 | elog.Print(err)
147 | } else {
148 | wsHub.broadcast <- string(jsonBytes)
149 | }
150 |
151 | case <-resultRequestChan:
152 | resultsJSON, err := json.Marshal(pingResults)
153 | if err != nil {
154 | elog.Print(err)
155 | }
156 |
157 | resultResponseChan <- resultsJSON
158 |
159 | case nps := <-pathResultsChan:
160 |
161 | pathName := nps.PathName
162 |
163 | if _, ok := paths[pathName]; !ok {
164 | paths[pathName] = nps
165 | } else {
166 | paths[pathName].AvgLatency = (paths[pathName].AvgLatency + nps.AvgLatency) / 2
167 | paths[pathName].MaxLatencyAvg = (paths[pathName].MaxLatencyAvg + nps.MaxLatencyAvg) / 2
168 | paths[pathName].TripCount++
169 | }
170 |
171 | case pr := <-hostResultsChan:
172 |
173 | hostName := pr.Target
174 |
175 | if _, ok := hosts[hostName]; !ok {
176 |
177 | hostDNSNames, err := net.LookupAddr(hostName)
178 | if err != nil {
179 | elog.Print(err)
180 | }
181 |
182 | var hostDNSName string
183 |
184 | if len(hostDNSNames) > 0 {
185 | hostDNSName = hostDNSNames[0]
186 | }
187 |
188 | hosts[hostName] = &HostStats{}
189 | if pr.Latency > 0 {
190 | hosts[hostName].MinLatency = pr.Latency
191 | } else {
192 | hosts[hostName].MinLatency = 9999
193 | }
194 | hosts[hostName].AvgLatency = pr.Latency
195 | hosts[hostName].HostName = hostName
196 | hosts[hostName].HostDNSName = hostDNSName
197 |
198 | } else {
199 |
200 | if hosts[hostName].MinLatency > pr.Latency {
201 | hosts[hostName].MinLatency = pr.Latency
202 | }
203 |
204 | if hosts[hostName].MaxLatency < pr.Latency {
205 | hosts[hostName].MaxLatency = pr.Latency
206 | }
207 |
208 | hosts[hostName].AvgLatency = (hosts[hostName].AvgLatency + pr.Latency) / 2
209 |
210 | }
211 |
212 | hosts[hostName].TripCount++
213 |
214 | if pr.Latency > 700 {
215 | hosts[hostName].HighLatency700++
216 | } else if pr.Latency > 400 {
217 | hosts[hostName].HighLatency400++
218 | } else if pr.Latency > 100 {
219 | hosts[hostName].HighLatency100++
220 | }
221 |
222 | case <-pathResultsRequestChan:
223 |
224 | prr := make(map[string]*PathStats)
225 |
226 | for k, v := range paths {
227 | prr[k] = v
228 | }
229 |
230 | pathResultsResponseChan <- paths
231 |
232 | case <-hostResultsRequestChan:
233 |
234 | hrr := make(map[string]*HostStats)
235 |
236 | for k, v := range hosts {
237 | hrr[k] = v
238 | }
239 |
240 | hostResultsResponseChan <- hosts
241 |
242 | }
243 |
244 | }
245 | }
246 |
247 | var thirtySampleRollingLatency []float64
248 |
249 | func init() {
250 | pingResults = make(map[string][]quack.PingResult)
251 | pingResultChan = make(chan quack.PingResult, 10)
252 | resultResponseChan = make(chan []byte, 10)
253 | resultRequestChan = make(chan int, 10)
254 | ttlTraceResultChan = make(chan []quack.PingResult, 10)
255 | unhealthyPingResultChan = make(chan quack.PingResult, 10)
256 |
257 | pathResultsChan = make(chan *PathStats, 10)
258 | pathResultsRequestChan = make(chan int, 10)
259 | pathResultsResponseChan = make(chan map[string]*PathStats, 10)
260 |
261 | hostResultsChan = make(chan *quack.PingResult, 10)
262 | hostResultsRequestChan = make(chan int, 10)
263 | hostResultsResponseChan = make(chan map[string]*HostStats, 10)
264 |
265 | elog = log.New(os.Stdout, "Error: ", log.LstdFlags|log.Lshortfile)
266 |
267 | }
268 |
269 | func echoResults(target string, packetsTx, packetsRx int, minLatency, avgLatency, maxLatency, stdDevLatency float64) {
270 |
271 | fmt.Print("\n")
272 | fmt.Printf("--- %s ping statistics ---\n", target)
273 | fmt.Printf("%d packets transmitted, %d packets received, %.1f%% packet loss\n", packetsTx, packetsRx, (float64(packetsTx-packetsRx) / float64(packetsTx) * 100))
274 | fmt.Printf("round-trip min/avg/max/stddev = %.3f/%.3f/%.3f/%.3f ms\n", minLatency, avgLatency, maxLatency, stdDevLatency)
275 | fmt.Printf("View charted results at: http://localhost:14445\n\n")
276 | }
277 |
278 | var resultFileNameSuffix string
279 |
280 | func main() {
281 |
282 | go resultProcessor()
283 |
284 | var maxLatency float64
285 | var minLatency float64 = 99999.9
286 | var avgLatency float64
287 | var stdDevLatency float64
288 | var packetsTx int
289 | var packetsRx int
290 |
291 | var target string
292 |
293 | //flag.StringVar(&target, "target", "8.8.8.8", "IPv4 address of target")
294 |
295 | //target := "8.8.8.8"
296 |
297 | re := regexp.MustCompile(`(!?\d{1,3})\.(!?\d{1,3})\.(!?\d{1,3})\.(!?\d{1,3})`)
298 |
299 | if len(os.Args) > 1 && re.Match([]byte(os.Args[1])) {
300 | target = os.Args[1]
301 | }
302 |
303 | flag.StringVar(&resultFileNameSuffix, "suffix", fmt.Sprintf("_%s", target), "filename suffix for storing result data (results[--].json)")
304 |
305 | flag.Parse()
306 |
307 | fmt.Printf("View charted results at: http://localhost:14445\n")
308 |
309 | pingTicker := time.NewTicker(time.Second)
310 | traceTicker := time.NewTicker(time.Second * 30)
311 | for {
312 | select {
313 | case <-pingTicker.C:
314 | packetsTx++
315 | latency, err := quack.SendPing(target, 0, pingResultChan)
316 | if err != nil {
317 | log.Print(err)
318 | fmt.Printf("Request timeout for icmp_seq %d\n", packetsTx)
319 | latency = 2000.0
320 | } else {
321 | packetsRx++
322 | }
323 |
324 | if avgLatency == 0 {
325 | avgLatency = latency
326 | } else {
327 | avgLatency = (avgLatency + latency) / 2
328 | }
329 |
330 | stdDevLatency += math.Pow(latency-avgLatency, 2)
331 | stdDevLatency = math.Sqrt(stdDevLatency / 2)
332 |
333 | if latency < minLatency {
334 | minLatency = latency
335 | }
336 |
337 | if latency > maxLatency {
338 | maxLatency = latency
339 | }
340 |
341 | if packetsTx%30 == 0 {
342 | echoResults(target, packetsTx, packetsRx, minLatency, avgLatency, maxLatency, stdDevLatency)
343 | }
344 |
345 | //time.Sleep(time.Duration(time.Second.Nanoseconds() - int64(latency*1000000)))
346 |
347 | if len(thirtySampleRollingLatency) == 30 {
348 | thirtySampleRollingLatency = thirtySampleRollingLatency[1:]
349 | }
350 | thirtySampleRollingLatency = append(thirtySampleRollingLatency, latency)
351 |
352 | case <-traceTicker.C:
353 | go func() {
354 | traceResults, err := quack.TTLTrace(target)
355 | if err != nil {
356 | elog.Print(err)
357 | }
358 |
359 | digestTraceResults(traceResults)
360 |
361 | ttlTraceResultChan <- traceResults
362 |
363 | var lastLatency float64
364 | var highLatencyHosts []quack.PingResult
365 | var latencyHighWaterMark float64
366 |
367 | for i, traceResult := range traceResults {
368 |
369 | //log.Printf("TTL %d result: %#v", i+1, traceResult)
370 |
371 | if lastLatency == 0 {
372 | lastLatency = traceResult.Latency
373 | continue
374 | }
375 |
376 | if math.Abs(traceResult.Latency-lastLatency) > 100 && traceResult.Latency != 0 && traceResult.Latency > latencyHighWaterMark {
377 | if i > 0 && traceResults[i-1].Latency > 100 {
378 | highLatencyHosts = append(highLatencyHosts, traceResults[i-1])
379 | }
380 | highLatencyHosts = append(highLatencyHosts, traceResult)
381 | }
382 | lastLatency = traceResult.Latency
383 |
384 | if traceResult.Latency > latencyHighWaterMark {
385 | latencyHighWaterMark = traceResult.Latency
386 | }
387 |
388 | }
389 |
390 | // If latency was high, perform additional ttlTraces to collect more path observations
391 |
392 | if latencyHighWaterMark > 100 {
393 | log.Printf("High latency of %.1fms detected. Performing additional traces.", latencyHighWaterMark)
394 | for x := 0; x < 4; x++ {
395 | traceResults, err := quack.TTLTrace(target)
396 | if err != nil {
397 | elog.Print(err)
398 | }
399 |
400 | _ = traceResults
401 |
402 | time.Sleep(time.Millisecond * 200)
403 | }
404 | }
405 |
406 | for _, highLatencyHost := range highLatencyHosts {
407 | log.Printf("Found potentially unhealthy host in trace: %#v", highLatencyHost)
408 |
409 | unhealthyPingResultChan <- highLatencyHost
410 | }
411 |
412 | }()
413 | }
414 | }
415 |
416 | }
417 |
418 | func digestTraceResults(traceResults []quack.PingResult) {
419 |
420 | var pathName string
421 | var targetNames []string
422 | var maxPathLatency float64
423 | var avgPathLatency float64
424 |
425 | for _, traceresult := range traceResults {
426 |
427 | targetNames = append(targetNames, traceresult.Target)
428 |
429 | tr := traceresult
430 |
431 | hostResultsChan <- &tr
432 |
433 | if avgPathLatency == 0 {
434 | avgPathLatency = traceresult.Latency
435 | } else {
436 | avgPathLatency = (avgPathLatency + traceresult.Latency) / 2
437 | }
438 | if maxPathLatency < traceresult.Latency {
439 | maxPathLatency = traceresult.Latency
440 | }
441 |
442 | }
443 |
444 | pathName = strings.Join(targetNames, "-")
445 |
446 | newPathStats := &PathStats{
447 | PathName: pathName,
448 | AvgLatency: avgPathLatency,
449 | MaxLatencyAvg: maxPathLatency,
450 | TripCount: 1,
451 | }
452 |
453 | pathResultsChan <- newPathStats
454 |
455 | }
456 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Duck long-term network latency tester
2 |
3 | How to use
4 | ---
5 |
6 | - Find your binary and run from the command line as administrator/root (for ICMP listener)
7 | - Watch the pings roll in
8 | - Browse to http://localhost:14445 and watch the results
9 | - Build from source with `go build`
10 | - Mac/Linux
11 | - Run duck with `sudo ./duck `
12 | - Windows
13 | - Run duck from administrator command prompt with `duck.exe `
14 |
15 | Screenshots
16 | ---
17 |
18 | 
19 | 
20 |
21 | Goals
22 | ---
23 |
24 | - Be a better, in-place replacement for terminal `ping` command
25 | - Capture high latency events in the background and make them clear on review
26 |
27 |
28 | Features
29 | ---
30 | - In-terminal ASCII latency charting
31 | - Colorization of latency in terminal (Mac and Linux only)
32 | - Statistics printed every 30 seconds
33 | - Live web dashboard for visualizing latency over extended tests
34 | - Long-term chart with 30-second averaged samples
35 | - Short-term chart (last 10 minutes) with live results
36 | - Traceroute results
37 | - Traceroute Path tracking with latency averaging
38 | - Individual traceroute host tracking with latency averaging and incident counting
39 | - Keeps history of ping performance older than 10 minutes as derezzed 30-second sample averages
40 | - Traceroute performed every 30 seconds
41 | - Paths stored and path traversal tracked
42 | - Host DNS lookup
43 | - Average and Max latency tracked over time
44 | - Stores pings, paths, and hosts to JSON files every minute
45 |
46 | Roadmap
47 | ---
48 | - Derezzing of ping results after 10 minutes
49 | - Exception to derezzeing is any high-latency event, with 5 minutes of high resolution context on either side
50 | - Variable control from WebUI
51 | - Payload size
52 | - Latency threshold adjustments
53 | - Jitter visualization
--------------------------------------------------------------------------------
/types.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/brendanporter/quack"
4 |
5 | type UnhealthyPingResult struct {
6 | Target string
7 | Latency float64
8 | Time int64
9 | MessageType string `json:"mt"`
10 | }
11 |
12 | type TraceResult struct {
13 | Hops []quack.PingResult
14 | MessageType string `json:"mt"`
15 | }
16 |
17 | type PathsResult struct {
18 | Paths map[string]*PathStats
19 | MessageType string `json:"mt"`
20 | }
21 |
22 | type HostsResult struct {
23 | Hosts map[string]*HostStats
24 | MessageType string `json:"mt"`
25 | }
26 |
27 | type HostStats struct {
28 | HostName string
29 | HostDNSName string
30 | AvgLatency float64
31 | MaxLatency float64
32 | MinLatency float64
33 | TripCount int
34 | HighLatency100 int
35 | HighLatency400 int
36 | HighLatency700 int
37 | }
38 |
39 | type PathStats struct {
40 | PathName string
41 | MaxLatencyAvg float64
42 | AvgLatency float64
43 | TripCount int
44 | }
45 |
--------------------------------------------------------------------------------
/windows/duck.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brendanporter/duck/87b520f1a9069767767199da6a20caf64c79a94c/windows/duck.exe
--------------------------------------------------------------------------------