Repository: digineo/go-ping Branch: master Commit: d7f777972058 Files: 39 Total size: 62.1 KB Directory structure: gitextract_u0ueinq6/ ├── .codecov.yml ├── .github/ │ ├── build-all │ └── workflows/ │ ├── build.yml │ └── golangci-lint.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── README.md ├── cmd/ │ ├── common.mk │ ├── multiping/ │ │ ├── Makefile │ │ ├── README.md │ │ ├── destination.go │ │ ├── destination_test.go │ │ ├── main.go │ │ ├── resolve.go │ │ └── ui.go │ ├── ping-monitor/ │ │ ├── Makefile │ │ └── main.go │ ├── ping-test/ │ │ ├── Makefile │ │ ├── README.md │ │ └── main.go │ └── pingnet/ │ ├── Makefile │ └── main.go ├── error.go ├── go.mod ├── go.sum ├── monitor/ │ ├── history.go │ ├── history_test.go │ ├── metrics.go │ ├── monitor.go │ └── target.go ├── payload.go ├── pinger.go ├── pinger_linux.go ├── pinger_other.go ├── pinger_test.go ├── receiving.go ├── request.go └── sending.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .codecov.yml ================================================ ignore: - cmd ================================================ FILE: .github/build-all ================================================ #!/bin/sh -e for mf in $(find cmd -name Makefile); do make -C "$(dirname $mf)" done ================================================ FILE: .github/workflows/build.yml ================================================ name: build on: - push - pull_request jobs: build: name: Tests runs-on: ubuntu-latest steps: - name: Set up Go 1.x uses: actions/setup-go@v5 with: go-version: ^1.23 id: go - name: Check out code into the Go module directory uses: actions/checkout@v5 - name: Build commands run: .github/build-all - name: Run tests run: sudo go test -v -coverprofile coverage.txt ./... - name: Upload coverage report uses: codecov/codecov-action@v5 with: files: coverage.txt ================================================ FILE: .github/workflows/golangci-lint.yml ================================================ name: golangci-lint on: - push - pull_request jobs: golangci: name: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-go@v5 with: go-version: ^1.23 - name: golangci-lint uses: golangci/golangci-lint-action@v8 with: version: v2.4.0 ================================================ FILE: .gitignore ================================================ # Binaries for programs and plugins *.exe *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ # Build fragments cmd/multiping/multiping cmd/multiping/multiping.log cmd/ping-test/ping-test cmd/ping-test/ping-test.log cmd/ping-monitor/ping-monitor cmd/pingnet/pingnet ================================================ FILE: .golangci.yml ================================================ version: "2" linters: exclusions: generated: lax presets: - comments - common-false-positives - legacy - std-error-handling rules: - linters: - errcheck path: \.go paths: - third_party$ - builtin$ - examples$ formatters: exclusions: generated: lax paths: - third_party$ - builtin$ - examples$ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 Digineo GmbH Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # go-ping [![GoDoc](https://godoc.org/github.com/digineo/go-ping?status.svg)](https://godoc.org/github.com/digineo/go-ping) [![Build Status](https://github.com/digineo/go-ping/workflows/build/badge.svg?branch=master)](https://github.com/digineo/go-ping/actions) [![Codecov](https://codecov.io/gh/digineo/go-ping/branch/master/graph/badge.svg)](https://codecov.io/gh/digineo/go-ping) [![Go Report Card](https://goreportcard.com/badge/github.com/digineo/go-ping)](https://goreportcard.com/report/github.com/digineo/go-ping) A simple ICMP Echo implementation, based on [golang.org/x/net/icmp][net-icmp]. Some sample programs are provided in `cmd/`: - [**`ping-test`**][ping-test] is a really simple ping clone - [**`multiping`**][multiping] provides an interactive TUI to ping multiple hosts - [**`ping-monitor`**][monitor] pings multiple hosts in parallel, but just prints the summary every so often - [**`pingnet`**][pingnet] allows to ping every host in a CIDR range (e.g. 0.0.0.0/0 :-)) [net-icmp]: https://godoc.org/golang.org/x/net/icmp [ping-test]: https://github.com/digineo/go-ping/tree/master/cmd/ping-test [multiping]: https://github.com/digineo/go-ping/tree/master/cmd/multiping [monitor]: https://github.com/digineo/go-ping/tree/master/cmd/ping-monitor [pingnet]: https://github.com/digineo/go-ping/tree/master/cmd/pingnet ## Features - [x] IPv4 and IPv6 support - [x] Unicast and multicast support - [x] configurable retry amount and timeout duration - [x] configurable payload size (and content) - [x] round trip time measurement ## Contribute Simply fork and create a pull-request. We'll try to respond in a timely fashion. ## Software using this library * [Ping Exporter for Prometheus](https://github.com/czerwonk/ping_exporter) Please create a pull request to get your software listed. ## License MIT License, Copyright (c) 2018 Digineo GmbH ================================================ FILE: cmd/common.mk ================================================ .PHONY: all all: $(TARGET) $(TARGET): go build -o $@ .PHONY: clean clean: rm -f $(TARGET) $(TARGET).log ================================================ FILE: cmd/multiping/Makefile ================================================ TARGET = multiping include ../common.mk .PHONY: test test: all go test ./... ./$(TARGET) golang.org cloudflare.com 2>$(TARGET).log cat $(TARGET).log ================================================ FILE: cmd/multiping/README.md ================================================ # multiping Just like regular `ping`, but measures round trip time to multiple hosts, all at once. The user interface is similar to [mtr](https://www.bitwizard.nl/mtr/): ![Screenshot of multiping in action](multiping.png) ## Installation Installing is as easy as: ``` $ go get -u github.com/digineo/go-ping/cmd/multiping ``` This will download and build this program and install it into `$GOPATH/bin/` (assuming you have the Go toolchain installed). To run it, you need elevated privileges (as mentioned in the [README of ping-test](../ping-test)). You can either run it as root (or via `sudo`; both not recommended), or enable the program to only open raw sockets (via `setcap`, but Linux-only): ``` $ sudo setcap cap_net_raw+ep $GOPATH/bin/multiping ``` ## Running Assuming `$GOPATH/bin` is in your `$PATH`, just call it with a list of hosts and IP addresses (currently only IPv4): ``` $ multiping golang.org google.com 127.0.0.1 ``` ### Options To get a list of available options, run `multiping -h`: ``` Usage of ./multiping: -bind4 string IPv4 bind address (default "0.0.0.0") -bind6 string IPv6 bind address (default "::") -buf uint buffer size for statistics (default 50) -interval duration polling interval (default 1s) -resolve duration timeout for DNS lookups (default 1.5s) -s uint size of payload in bytes (default 56) -timeout duration timeout for a single echo request (default 1s) ``` ## Roadmap - [x] cleanup UI code (this is a bit of a mess) - [ ] add more features - [ ] different display modes (`mtr` has different views) - [x] move "last error" column into a log area at the bottom - [ ] increase/decrease interval and/or timeout with `-`/`+` keys - [x] fill IPv6 options with life (once the library has IPv6 support) - [x] use something more sophisticated than `net.ResolveIPAddress` to get a list of all A/AAAA records for a given domain name - [ ] this propably needs an off-switch ================================================ FILE: cmd/multiping/destination.go ================================================ package main import ( "log" "math" "net" "sync" "time" ping "github.com/digineo/go-ping" ) type history struct { received int lost int results []time.Duration // ring, start index = .received%len mtx sync.RWMutex } type destination struct { host string remote *net.IPAddr *history } type stat struct { pktSent int pktLoss float64 last time.Duration best time.Duration worst time.Duration mean time.Duration stddev time.Duration } func (u *destination) ping(pinger *ping.Pinger) { rtt, err := pinger.Ping(u.remote, opts.timeout) if err != nil { log.Printf("[yellow]%s[white]: %v", u.host, err) } u.addResult(rtt, err) } func (s *history) addResult(rtt time.Duration, err error) { s.mtx.Lock() if err == nil { s.results[s.received%len(s.results)] = rtt s.received++ } else { s.lost++ } s.mtx.Unlock() } func (s *history) compute() (st stat) { s.mtx.RLock() defer s.mtx.RUnlock() if s.received == 0 { if s.lost > 0 { st.pktLoss = 1.0 } return } collection := s.results[:] st.pktSent = s.received + s.lost size := len(s.results) st.last = collection[(s.received-1)%size] // we don't yet have filled the buffer if s.received <= size { collection = s.results[:s.received] size = s.received } st.pktLoss = float64(s.lost) / float64(s.received+s.lost) st.best, st.worst = collection[0], collection[0] total := time.Duration(0) for _, rtt := range collection { if rtt < st.best { st.best = rtt } if rtt > st.worst { st.worst = rtt } total += rtt } st.mean = time.Duration(float64(total) / float64(size)) stddevNum := float64(0) for _, rtt := range collection { dev := float64(rtt - st.mean) stddevNum += dev * dev } st.stddev = time.Duration(math.Sqrt(stddevNum / float64(size))) return } ================================================ FILE: cmd/multiping/destination_test.go ================================================ package main import ( "testing" "time" "github.com/stretchr/testify/assert" ) func TestComputeStats(t *testing.T) { assert := assert.New(t) const ( z = time.Duration(0) ms = time.Millisecond µs = time.Microsecond ns = time.Nanosecond ) testcases := []struct { title string results []time.Duration received int lost int last time.Duration best time.Duration worst time.Duration mean time.Duration stddev time.Duration loss float64 }{ { title: "simplest case", results: []time.Duration{}, received: 0, last: z, best: z, worst: z, mean: z, stddev: z, }, { title: "another simple case", results: []time.Duration{ms}, received: 1, last: ms, best: ms, worst: ms, mean: ms, stddev: z, }, { title: "same as before, but sent>len(res)", results: []time.Duration{ms}, received: 3, last: ms, best: ms, worst: ms, mean: ms, stddev: z, }, { title: "same as before, but sent int(^byte(0)) { // Too many targets? fmt.Println("Too many targets") os.Exit(1) } // Bind to sockets if p, err := ping.New("0.0.0.0", "::"); err != nil { fmt.Printf("Unable to bind: %s\nRunning as root?\n", err) os.Exit(2) } else { pinger = p } pinger.SetPayloadSize(uint16(size)) defer pinger.Close() // Create monitor monitor := monitor.New(pinger, pingInterval, pingTimeout) defer monitor.Stop() // Add targets targets = flag.Args() for i, target := range targets { ipAddr, err := net.ResolveIPAddr("", target) if err != nil { fmt.Printf("invalid target '%s': %s", target, err) continue } monitor.AddTargetDelayed(string([]byte{byte(i)}), *ipAddr, 10*time.Millisecond*time.Duration(i)) } // Start report routine ticker := time.NewTicker(reportInterval) defer ticker.Stop() go func() { for range ticker.C { for i, metrics := range monitor.ExportAndClear() { fmt.Printf("%s: %+v\n", targets[[]byte(i)[0]], *metrics) } } }() // Handle SIGINT and SIGTERM. ch := make(chan os.Signal, 1) signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) fmt.Println("received", <-ch) } ================================================ FILE: cmd/ping-test/Makefile ================================================ TARGET = ping-test include ../common.mk .PHONY: test test: all @truncate -s0 $(TARGET).log @./$(TARGET) -4 golang.org 2>>$(TARGET).log @./$(TARGET) -6 golang.org 2>>$(TARGET).log @./$(TARGET) -4 cloudflare.com 2>>$(TARGET).log @./$(TARGET) -6 cloudflare.com 2>>$(TARGET).log @echo error log: @cat $(TARGET).log ================================================ FILE: cmd/ping-test/README.md ================================================ # ping-test This is a sample program to demonstrate the usage of this library. It is not intended for production use. ## How-to **Building/installing** does not require any special steps. Either run `go get` (to install it directly in `$GOPATH/bin/ping-test`) ``` $ go get -u github.com/digineo/go-ping/cmd/ping-test ``` or skip installing it (and build it yourself): ``` $ go get -u -d github.com/digineo/go-ping $ cd $GOPATH/src/github.com/digineo/go-ping/cmd/ping-test $ go build # this creates ./ping-test ``` **Running** `ping-test` requires elevated privileges, since normal users cannot open ICMP sockets. To circumvent this, either run the binary as root, e.g. via `sudo` *(not recommended!)* ``` $ sudo ./ping-test -4 golang.org ping golang.org (216.58.211.113) rtt=11.869403ms $ sudo ./ping-test -6 golang.org ping golang.org (2a00:1450:400e:809::2011) rtt=11.412907ms ``` Better yet, allow the binary to only open raw sockets (via `capabilities(7)`): ``` $ sudo setcap cap_net_raw+ep ./ping-test $ ./ping-test -4 golang.org ping golang.org (216.58.211.113) rtt=11.772573ms $ ./ping-test -6 golang.org ping golang.org (2a00:1450:400e:809::2011) rtt=11.31439ms ``` Note, that you'll need to re-apply the `setcap` command everytime the binary changes (i.e. after `go build`). Also, since configuring the system capabilities is a Linux feature, you may need to resort to Docker or VM environments, if you want to try this binary, but don't trust its source code. Or you like living in the danger zone and don't mind the occasional system crash introduced by running code found on the internet with root privileges :-) ================================================ FILE: cmd/ping-test/main.go ================================================ package main import ( "flag" "fmt" "log" "net" "os" "time" ping "github.com/digineo/go-ping" ) var ( args []string attempts uint = 3 timeout = time.Second proto4, proto6 bool size uint = 56 bind string destination string remoteAddr *net.IPAddr pinger *ping.Pinger ) func main() { flag.Usage = func() { fmt.Fprintln(os.Stderr, "Usage:", os.Args[0], "[options] host [host [...]]") flag.PrintDefaults() } flag.UintVar(&attempts, "attempts", attempts, "number of attempts") flag.DurationVar(&timeout, "timeout", timeout, "timeout for a single echo request") flag.UintVar(&size, "s", size, "size of additional payload data") flag.BoolVar(&proto4, "4", proto4, "use IPv4 (mutually exclusive with -6)") flag.BoolVar(&proto6, "6", proto6, "use IPv6 (mutually exclusive with -4)") flag.StringVar(&bind, "bind", "", "IPv4 or IPv6 bind address (defaults to 0.0.0.0 for IPv4 and :: for IPv6)") flag.Parse() if proto4 == proto6 { log.Fatalf("need exactly one of -4 and -6 flags") } if bind == "" { if proto4 { bind = "0.0.0.0" } else if proto6 { bind = "::" } } args := flag.Args() destination := args[0] if proto4 { if r, err := net.ResolveIPAddr("ip4", destination); err != nil { panic(err) } else { remoteAddr = r } if p, err := ping.New(bind, ""); err != nil { panic(err) } else { pinger = p } } else if proto6 { if r, err := net.ResolveIPAddr("ip6", destination); err != nil { panic(err) } else { remoteAddr = r } if p, err := ping.New("", bind); err != nil { panic(err) } else { pinger = p } } defer pinger.Close() if pinger.PayloadSize() != uint16(size) { pinger.SetPayloadSize(uint16(size)) } if remoteAddr.IP.IsLinkLocalMulticast() { multicastPing() } else { unicastPing() } } func unicastPing() { rtt, err := pinger.PingAttempts(remoteAddr, timeout, int(attempts)) if err != nil { fmt.Println(err) os.Exit(1) } fmt.Printf("ping %s (%s) rtt=%v\n", destination, remoteAddr, rtt) } func multicastPing() { fmt.Printf("multicast ping to %s (%s)\n", args[0], destination) responses, err := pinger.PingMulticast(remoteAddr, timeout) if err != nil { fmt.Println(err) os.Exit(1) } for response := range responses { fmt.Printf("%+v\n", response) } } ================================================ FILE: cmd/pingnet/Makefile ================================================ TARGET = pingnet include ../common.mk .PHONY: test test: all ./$(TARGET) -c 1 -w 100ms -f 127.0.0.1/24 ================================================ FILE: cmd/pingnet/main.go ================================================ package main import ( "flag" "fmt" "log" "net" "os" "runtime" "strings" "sync" "time" ping "github.com/digineo/go-ping" "gopkg.in/cheggaaa/pb.v1" ) var ( timeout = 5 * time.Second attempts = 3 poolSize = 2 * runtime.NumCPU() interval = 100 * time.Millisecond ifname = "" bind6 = "::" bind4 = "0.0.0.0" size = uint(56) force bool verbose bool mark uint pinger *ping.Pinger ) type workGenerator struct { ip net.IP net *net.IPNet } func (w *workGenerator) size() uint64 { ones, bits := w.net.Mask.Size() return 1 << uint64(bits-ones) } func (w *workGenerator) each(callback func(net.IP) error) error { // adapted from http://play.golang.org/p/m8TNTtygK0 inc := func(ip net.IP) net.IP { res := make(net.IP, len(ip)) copy(res, ip) for j := len(res) - 1; j >= 0; j-- { res[j]++ if res[j] > 0 { break } } return res } for ip := w.ip.Mask(w.net.Mask); w.net.Contains(ip); ip = inc(ip) { if err := callback(ip); err != nil { return err } } return nil } type result struct { addr net.IPAddr rtt time.Duration err error } func main() { log.SetFlags(0) flag.Usage = func() { fmt.Fprintln(os.Stderr, "Usage:", os.Args[0], "[options] CIDR [CIDR [...]]") flag.PrintDefaults() } flag.IntVar(&attempts, "c", attempts, "number of ping attempts per address") flag.DurationVar(&timeout, "w", timeout, "timeout for a single echo request") flag.DurationVar(&interval, "i", interval, "CIDR iteration interval") flag.UintVar(&size, "s", size, "size of additional payload data") flag.StringVar(&bind4, "4", bind4, "IPv4 bind address") flag.StringVar(&bind6, "6", bind6, "IPv6 bind address") flag.StringVar(&ifname, "I", ifname, "interface name/IPv6 zone") flag.IntVar(&poolSize, "P", poolSize, "concurrency level") flag.BoolVar(&force, "f", force, "sanity flag needed if you want to ping more than 4096 hosts (/20)") flag.BoolVar(&verbose, "v", verbose, "also print out unreachable addresses") flag.UintVar(&mark, "m", mark, "set socket mark (SO_MARK) to this value") flag.Parse() // simple error checking if bind4 == "" && bind6 == "" { log.Fatal("need at least an IPv4 (-bind4 flag) or IPv6 (-bind6 flag) address to bind to") } if poolSize <= 0 { log.Fatal("concurrency level (-P flag) must be > 0") } if attempts <= 0 { log.Fatal("number of ping attempts (-c flag) must be > 0") } // parse CIDR arguments total := uint64(0) generator := make([]*workGenerator, 0, flag.NArg()) for _, cidr := range flag.Args() { ip, ipnet, err := net.ParseCIDR(cidr) if err != nil { log.Println(err) continue } w := &workGenerator{ip: ip, net: ipnet} generator = append(generator, w) total += w.size() } if total == 0 { // no (valid) CIDR argument given flag.Usage() os.Exit(1) } else if total > 4096 && !force { // expanding all arguments yields too many addresses log.Printf("You want to ping %d hosts. If that is correct, try again with -f flag", total) os.Exit(1) } if p, err := ping.New(bind4, bind6); err != nil { log.Fatal(err) } else { pinger = p } if mark > 0 { pinger.SetMark(mark) } // prepare worker wg := &sync.WaitGroup{} wg.Add(poolSize) ips := make(chan net.IPAddr, poolSize) res := make(chan *result, poolSize) for i := 0; i < poolSize; i++ { go func() { for ip := range ips { var err error var rtt time.Duration for i := 1; ; i++ { rtt, err = pinger.PingAttempts(&ip, timeout, attempts) if err == nil || !strings.Contains(err.Error(), "no buffer space available") { break } time.Sleep(timeout * time.Duration(i)) } res <- &result{addr: ip, rtt: rtt, err: err} } wg.Done() }() } // printer pr := &sync.WaitGroup{} pr.Add(1) go func() { bar := pb.New64(int64(total)) bar.ShowBar = true bar.ShowTimeLeft = true bar.ShowCounters = true bar.Start() const clear = "\x1b[2K\r" // ansi delete line + CR for r := range res { bar.Increment() if r.err == nil { log.Printf("%s%s - rtt=%v", clear, r.addr.IP, r.rtt) bar.Update() } else if verbose { log.Printf("%s%s - %v", clear, r.addr, r.err) bar.Update() } } bar.Finish() pr.Done() }() // yield all IP addresses for _, g := range generator { g.each(func(ip net.IP) error { ips <- net.IPAddr{IP: ip, Zone: ifname} time.Sleep(interval) return nil }) } // wait for worker and printer to finish close(ips) wg.Wait() close(res) pr.Wait() } ================================================ FILE: error.go ================================================ package ping import "errors" var ( errClosed = errors.New("pinger closed") errNotBound = errors.New("need at least one bind address") ) // timeoutError implements the net.Error interface. Originally taken from // https://github.com/golang/go/blob/release-branch.go1.8/src/net/net.go#L505-L509 type timeoutError struct{} func (e *timeoutError) Error() string { return "i/o timeout" } func (e *timeoutError) Timeout() bool { return true } func (e *timeoutError) Temporary() bool { return true } ================================================ FILE: go.mod ================================================ module github.com/digineo/go-ping go 1.23.0 require ( github.com/digineo/go-logwrap v0.0.0-20181106161722-a178c58ea3f0 github.com/gdamore/tcell/v2 v2.9.0 github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb github.com/stretchr/testify v1.11.0 golang.org/x/net v0.43.0 gopkg.in/cheggaaa/pb.v1 v1.0.28 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.7.0 // indirect github.com/gdamore/encoding v1.0.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect golang.org/x/sys v0.35.0 // indirect golang.org/x/term v0.34.0 // indirect golang.org/x/text v0.28.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/digineo/go-logwrap v0.0.0-20181106161722-a178c58ea3f0 h1:OT/LKmj81wMymnWXaKaKBR9n1vPlu+GC0VVKaZP6kzs= github.com/digineo/go-logwrap v0.0.0-20181106161722-a178c58ea3f0/go.mod h1:DmqdumeAKGQNU5E8MN0ruT5ZGx8l/WbAsMbXCXcSEts= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= github.com/gdamore/tcell/v2 v2.9.0 h1:N6t+eqK7/xwtRPwxzs1PXeRWnm0H9l02CrgJ7DLn1ys= github.com/gdamore/tcell/v2 v2.9.0/go.mod h1:8/ZoqM9rxzYphT9tH/9LnunhV9oPBqwS8WHGYm5nrmo= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.0 h1:v2XXALHHh6zHfYTJ+cSkwtyffnaOyR1MXaA91mTrb8o= github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb h1:n7UJ8X9UnrTZBYXnd1kAIBc067SWyuPIrsocjketYW8= github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8= github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk= gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: monitor/history.go ================================================ package monitor import ( "math" "sort" "sync" "time" ) // Result stores the information about a single ping, in particular // the round-trip time or whether the packet was lost. type Result struct { RTT time.Duration Lost bool } // History represents the ping history for a single node/device. type History struct { results []Result count int position int sync.RWMutex } // NewHistory creates a new History object with a specific capacity func NewHistory(capacity int) History { return History{ results: make([]Result, capacity), } } // AddResult saves a ping result into the internal history. func (h *History) AddResult(rtt time.Duration, err error) { h.Lock() h.results[h.position] = Result{RTT: rtt, Lost: err != nil} h.position = (h.position + 1) % cap(h.results) if h.count < cap(h.results) { h.count++ } h.Unlock() } func (h *History) clear() { h.count = 0 h.position = 0 } // ComputeAndClear aggregates the result history into a single data point and clears the result set. func (h *History) ComputeAndClear() *Metrics { h.Lock() result := h.compute() h.clear() h.Unlock() return result } // Compute aggregates the result history into a single data point. func (h *History) Compute() *Metrics { h.RLock() defer h.RUnlock() return h.compute() } func (h *History) compute() *Metrics { numFailure := 0 numTotal := h.count µsPerMs := 1.0 / float64(time.Millisecond) if numTotal == 0 { return nil } data := make([]float64, 0, numTotal) var best, worst, mean, stddev, total, sumSquares float64 var extremeFound bool for i := 0; i < numTotal; i++ { curr := &h.results[i] if curr.Lost { numFailure++ } else { rtt := float64(curr.RTT) * µsPerMs data = append(data, rtt) if !extremeFound || rtt < best { best = rtt } if !extremeFound || rtt > worst { worst = rtt } extremeFound = true total += rtt } } size := float64(numTotal - numFailure) mean = total / size for _, rtt := range data { diff := rtt - mean sumSquares += diff * diff } stddev = math.Sqrt(sumSquares / size) median := math.NaN() if l := len(data); l > 0 { sort.Float64Slice(data).Sort() if l%2 == 0 { median = (data[l/2-1] + data[l/2]) / 2 } else { median = data[l/2] } } return &Metrics{ PacketsSent: numTotal, PacketsLost: numFailure, Best: float32(best), Worst: float32(worst), Median: float32(median), Mean: float32(mean), StdDev: float32(stddev), } } ================================================ FILE: monitor/history_test.go ================================================ package monitor import ( "fmt" "math" "testing" "time" "github.com/stretchr/testify/assert" ) func BenchmarkAddResult(b *testing.B) { h := NewHistory(8) for i := 0; i < b.N; i++ { h.AddResult(time.Duration(i), nil) // 1 allocc } } func BenchmarkCompute(b *testing.B) { h := NewHistory(8) for i := 0; i < b.N; i++ { h.AddResult(time.Duration(i), nil) // 1 alloc h.Compute() // 2 allocs } } func TestCompute(t *testing.T) { assert := assert.New(t) const dur = 100 * time.Millisecond err := fmt.Errorf("i/o timeout") { // empty list h := NewHistory(4) assert.Nil(h.Compute()) } { // one failed entry h := NewHistory(4) h.AddResult(2, err) metrics := h.Compute() assert.EqualValues(1, metrics.PacketsSent) assert.EqualValues(1, metrics.PacketsLost) assert.EqualValues(0, metrics.Best) assert.EqualValues(0, metrics.Worst) assert.True(math.IsNaN(float64(metrics.Median))) assert.True(math.IsNaN(float64(metrics.Mean))) assert.True(math.IsNaN(float64(metrics.StdDev))) } { // populate with 5 entries h := NewHistory(8) h.AddResult(0, nil) h.AddResult(dur, nil) h.AddResult(dur, nil) h.AddResult(0, err) h.AddResult(dur, nil) assert.Equal(h.count, 5) assert.EqualValues(1, h.Compute().PacketsLost) } { // test median h := NewHistory(5) h.AddResult(3*dur, nil) h.AddResult(2*dur, nil) h.AddResult(1*dur, nil) h.AddResult(0*dur, nil) assert.EqualValues(150, h.Compute().Median) h.AddResult(4*dur, nil) assert.EqualValues(200, h.Compute().Median) } { // test zero variance h := NewHistory(8) h.AddResult(dur, nil) h.AddResult(dur, nil) h.AddResult(0, err) metrics := h.Compute() assert.EqualValues(100, metrics.Best) assert.EqualValues(100, metrics.Worst) assert.EqualValues(100, metrics.Mean) assert.EqualValues(100, metrics.Median) assert.EqualValues(0, metrics.StdDev) assert.EqualValues(3, metrics.PacketsSent) assert.EqualValues(1, metrics.PacketsLost) // results getting worse h.AddResult(2*dur, nil) h.AddResult(dur, nil) h.AddResult(0, err) metrics = h.Compute() assert.EqualValues(100, metrics.Best) assert.EqualValues(200, metrics.Worst) assert.EqualValues(125, metrics.Mean) assert.EqualValues(100, metrics.Median) assert.InDelta(43.30127, float64(metrics.StdDev), 0.000001) assert.EqualValues(6, metrics.PacketsSent) assert.EqualValues(2, metrics.PacketsLost) // finally something better h.AddResult(0, nil) metrics = h.Compute() assert.EqualValues(0, metrics.Best) assert.EqualValues(200, metrics.Worst) assert.EqualValues(100, metrics.Mean) assert.EqualValues(100, metrics.Median) assert.InDelta(63.2455, float64(metrics.StdDev), 0.0001) assert.EqualValues(7, metrics.PacketsSent) assert.EqualValues(2, metrics.PacketsLost) } } func TestHistoryCapacity(t *testing.T) { assert := assert.New(t) err := fmt.Errorf("i/o timeout") h := NewHistory(3) assert.Equal(h.count, 0) h.AddResult(1, nil) h.AddResult(2, err) assert.Equal(h.count, 2) assert.Equal(h.position, 2) h.AddResult(1, nil) assert.Equal(h.count, 3) assert.Equal(h.position, 0) h.AddResult(0, nil) assert.Equal(h.count, 3) assert.Equal(h.position, 1) assert.EqualValues(1, h.Compute().PacketsLost) // overwrite lost packet result h.AddResult(0, nil) assert.EqualValues(0, h.Compute().PacketsLost) // clear h.ComputeAndClear() assert.Equal(h.count, 0) assert.Equal(h.position, 0) } ================================================ FILE: monitor/metrics.go ================================================ package monitor // Metrics is a dumb data point computed from a history of Results. type Metrics struct { PacketsSent int // number of packets sent PacketsLost int // number of packets lost Best float32 // best rtt in ms Worst float32 // worst rtt in ms Median float32 // median rtt in ms Mean float32 // mean rtt in ms StdDev float32 // std deviation in ms } ================================================ FILE: monitor/monitor.go ================================================ package monitor import ( "net" "sync" "time" "github.com/digineo/go-ping" ) // Monitor manages the goroutines responsible for collecting Ping RTT data. type Monitor struct { HistorySize int // Number of results per target to keep pinger *ping.Pinger interval time.Duration targets map[string]*Target mtx sync.RWMutex timeout time.Duration } const defaultHistorySize = 10 // New creates and configures a new Ping instance. You need to call // AddTarget()/RemoveTarget() to manage monitored targets. func New(pinger *ping.Pinger, interval, timeout time.Duration) *Monitor { return &Monitor{ pinger: pinger, interval: interval, timeout: timeout, targets: make(map[string]*Target), HistorySize: defaultHistorySize, } } // Stop brings the monitoring gracefully to a halt. func (p *Monitor) Stop() { p.mtx.Lock() for id := range p.targets { p.removeTarget(id) } p.pinger.Close() p.mtx.Unlock() } // AddTarget adds a target to the monitored list. If the target with the given // ID already exists, it is removed first and then readded. This allows // the easy restart of the monitoring. func (p *Monitor) AddTarget(key string, addr net.IPAddr) (err error) { return p.AddTargetDelayed(key, addr, 0) } // AddTargetDelayed is AddTarget with a startup delay func (p *Monitor) AddTargetDelayed(key string, addr net.IPAddr, startupDelay time.Duration) (err error) { p.mtx.Lock() defer p.mtx.Unlock() target, err := newTarget(p.interval, p.timeout, startupDelay, p.HistorySize, p.pinger, addr) if err != nil { return err } p.removeTarget(key) p.targets[key] = target return } // RemoveTarget removes a target from the monitoring list. func (p *Monitor) RemoveTarget(key string) { p.mtx.Lock() p.removeTarget(key) p.mtx.Unlock() } // Stops monitoring a target and removes it from the list (if the list includes // the target). Needs to be locked externally! func (p *Monitor) removeTarget(key string) { target, found := p.targets[key] if !found { return } target.Stop() delete(p.targets, key) } // ExportAndClear calculates the metrics for each monitored target, cleans the result set and // returns it as a simple map. func (p *Monitor) ExportAndClear() map[string]*Metrics { return p.export(true) } // Export calculates the metrics for each monitored target and returns it as a simple map. func (p *Monitor) Export() map[string]*Metrics { return p.export(false) } func (p *Monitor) export(clear bool) map[string]*Metrics { m := make(map[string]*Metrics) p.mtx.RLock() defer p.mtx.RUnlock() for id, target := range p.targets { if metrics := target.Compute(clear); metrics != nil { m[id] = metrics } } return m } ================================================ FILE: monitor/target.go ================================================ package monitor import ( "net" "sync" "time" ping "github.com/digineo/go-ping" ) // Target is a unit of work type Target struct { pinger *ping.Pinger addr net.IPAddr interval time.Duration timeout time.Duration stop chan struct{} history History wg sync.WaitGroup } // newTarget starts a new monitoring goroutine func newTarget(interval, timeout, startupDelay time.Duration, historySize int, pinger *ping.Pinger, addr net.IPAddr) (*Target, error) { n := &Target{ pinger: pinger, addr: addr, interval: interval, timeout: timeout, stop: make(chan struct{}), history: NewHistory(historySize), } n.wg.Add(1) go n.run(startupDelay) return n, nil } func (n *Target) run(startupDelay time.Duration) { if startupDelay > 0 { select { case <-time.After(startupDelay): case <-n.stop: } } tick := time.NewTicker(n.interval) for { select { case <-n.stop: tick.Stop() n.wg.Done() return case <-tick.C: go n.ping() } } } // Stop gracefully stops the monitoring. func (n *Target) Stop() { close(n.stop) n.wg.Wait() } // Compute returns the computed ping metrics for this node and optonally clears the result set. func (n *Target) Compute(clear bool) *Metrics { if clear { return n.history.ComputeAndClear() } return n.history.Compute() } func (n *Target) ping() { n.history.AddResult(n.pinger.Ping(&n.addr, n.timeout)) } ================================================ FILE: payload.go ================================================ package ping import ( "math/rand" "time" "github.com/digineo/go-logwrap" ) var ( log = &logwrap.Instance{} // SetLogger allows updating the Logger. For details, see // "github.com/digineo/go-logwrap".Instance.SetLogger. SetLogger = log.SetLogger // SA1019: rand.Seed has been deprecated, provide package-local RNG rng = rand.New(rand.NewSource(time.Now().UnixNano())) ) // Payload represents additional data appended to outgoing ICMP Echo // Requests. type Payload []byte // Resize will assign a new payload of the given size to p. func (p *Payload) Resize(size uint16) { buf := make([]byte, size) if _, err := rng.Read(buf); err != nil { log.Errorf("error resizing payload: %v", err) return } *p = Payload(buf) } ================================================ FILE: pinger.go ================================================ package ping import ( "net" "os" "sync" "golang.org/x/net/icmp" ) const ( // ProtocolICMP is the number of the Internet Control Message Protocol // (see golang.org/x/net/internal/iana.ProtocolICMP) ProtocolICMP = 1 // ProtocolICMPv6 is the IPv6 Next Header value for ICMPv6 // see golang.org/x/net/internal/iana.ProtocolIPv6ICMP ProtocolICMPv6 = 58 ) // default sequence counter for this process var sequence uint32 // Pinger is a instance for ICMP echo requests type Pinger struct { LogUnexpectedPackets bool // increases log verbosity Id uint16 SequenceCounter *uint32 payload Payload payloadMu sync.RWMutex requests map[uint32]request // currently running requests mtx sync.RWMutex // lock for the requests map conn4 net.PacketConn conn6 net.PacketConn write4 sync.Mutex // lock for conn4.WriteTo write6 sync.Mutex // lock for conn6.WriteTo wg sync.WaitGroup } // New creates a new Pinger. This will open the raw socket and start the // receiving logic. You'll need to call Close() to cleanup. func New(bind4, bind6 string) (*Pinger, error) { // open sockets conn4, err := connectICMP("ip4:icmp", bind4) if err != nil { return nil, err } conn6, err := connectICMP("ip6:ipv6-icmp", bind6) if err != nil { if conn4 != nil { conn4.Close() } return nil, err } if conn4 == nil && conn6 == nil { return nil, errNotBound } pinger := Pinger{ conn4: conn4, conn6: conn6, Id: uint16(os.Getpid()), SequenceCounter: &sequence, requests: make(map[uint32]request), } pinger.SetPayloadSize(56) if conn4 != nil { pinger.wg.Add(1) go pinger.receiver(ProtocolICMP, pinger.conn4) } if conn6 != nil { pinger.wg.Add(1) go pinger.receiver(ProtocolICMPv6, pinger.conn6) } return &pinger, nil } // Close will close the ICMP socket. func (pinger *Pinger) Close() { pinger.close(pinger.conn4) pinger.close(pinger.conn6) pinger.wg.Wait() } // connectICMP opens a new ICMP connection, if network and address are not empty. func connectICMP(network, address string) (*icmp.PacketConn, error) { if network == "" || address == "" { return nil, nil } return icmp.ListenPacket(network, address) } func (pinger *Pinger) close(conn net.PacketConn) { if conn != nil { conn.Close() } } func (pinger *Pinger) removeRequest(idseq uint32) { pinger.mtx.Lock() delete(pinger.requests, idseq) pinger.mtx.Unlock() } // SetPayloadSize resizes additional payload data to the given size. The // payload will subsequently be appended to outgoing ICMP Echo Requests. // // The default payload size is 56, resulting in 64 bytes for the ICMP packet. func (pinger *Pinger) SetPayloadSize(size uint16) { pinger.payloadMu.Lock() pinger.payload.Resize(size) pinger.payloadMu.Unlock() } // SetPayload allows you to overwrite the current payload with your own data. func (pinger *Pinger) SetPayload(data []byte) { pinger.payloadMu.Lock() pinger.payload = Payload(data) pinger.payloadMu.Unlock() } // PayloadSize retrieves the current payload size. func (pinger *Pinger) PayloadSize() uint16 { pinger.payloadMu.RLock() defer pinger.payloadMu.RUnlock() return uint16(len(pinger.payload)) } ================================================ FILE: pinger_linux.go ================================================ package ping import ( "errors" "os" "reflect" "syscall" "golang.org/x/net/icmp" ) // getFD gets the system file descriptor for an icmp.PacketConn func getFD(c *icmp.PacketConn) (uintptr, error) { v := reflect.ValueOf(c).Elem().FieldByName("c").Elem() if v.Elem().Kind() != reflect.Struct { return 0, errors.New("invalid type") } fd := v.Elem().FieldByName("conn").FieldByName("fd") if fd.Elem().Kind() != reflect.Struct { return 0, errors.New("invalid type") } pfd := fd.Elem().FieldByName("pfd") if pfd.Kind() != reflect.Struct { return 0, errors.New("invalid type") } return uintptr(pfd.FieldByName("Sysfd").Int()), nil } func (pinger *Pinger) SetMark(mark uint) error { conn4, ok := pinger.conn4.(*icmp.PacketConn) if !ok { return errors.New("invalid connection type") } fd, err := getFD(conn4) if err != nil { return err } err = os.NewSyscallError( "setsockopt", syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(mark)), ) if err != nil { return err } conn6, ok := pinger.conn6.(*icmp.PacketConn) if !ok { return errors.New("invalid connection type") } fd, err = getFD(conn6) if err != nil { return err } return os.NewSyscallError( "setsockopt", syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(mark)), ) } ================================================ FILE: pinger_other.go ================================================ //go:build !linux package ping import "errors" func (pinger *Pinger) SetMark(mark uint) error { return errors.New("setting SO_MARK socket option is not supported on this platform") } ================================================ FILE: pinger_test.go ================================================ package ping import ( "net" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestPinger(t *testing.T) { assert := assert.New(t) require := require.New(t) pinger, err := New("0.0.0.0", "::") require.NoError(err) require.NotNil(pinger) defer pinger.Close() for _, target := range []string{"127.0.0.1", "::1"} { rtt, err := pinger.PingAttempts(&net.IPAddr{IP: net.ParseIP(target)}, time.Second, 2) assert.NoError(err, target) assert.NotZero(rtt, target) } } ================================================ FILE: receiving.go ================================================ package ping import ( "fmt" "net" "time" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" ) // receiver listens on the raw socket and correlates ICMP Echo Replys with // currently running requests. func (pinger *Pinger) receiver(proto int, conn net.PacketConn) { rb := make([]byte, 1500) // read incoming packets for { if n, source, err := conn.ReadFrom(rb); err != nil { if netErr, ok := err.(net.Error); !ok || !netErr.Temporary() { //nolint:staticcheck break // socket gone } } else { pinger.receive(proto, rb[:n], source.(*net.IPAddr).IP, time.Now()) } } // close running requests pinger.mtx.RLock() for _, req := range pinger.requests { req.handleReply(errClosed, nil, nil) } pinger.mtx.RUnlock() // Close() waits for us pinger.wg.Done() } // receive takes the raw message and tries to evaluate an ICMP response. // If that succeeds, the body will given to process() for further processing. func (pinger *Pinger) receive(proto int, bytes []byte, addr net.IP, t time.Time) { // parse message m, err := icmp.ParseMessage(proto, bytes) if err != nil { return } // evaluate message switch m.Type { case ipv4.ICMPTypeEchoReply, ipv6.ICMPTypeEchoReply: pinger.process(m.Body, nil, addr, &t) case ipv4.ICMPTypeDestinationUnreachable, ipv6.ICMPTypeDestinationUnreachable: body := m.Body.(*icmp.DstUnreach) if body == nil { return } var bodyData []byte switch proto { case ProtocolICMP: // parse header of original IPv4 packet hdr, err := ipv4.ParseHeader(body.Data) if err != nil { return } bodyData = body.Data[hdr.Len:] case ProtocolICMPv6: // parse header of original IPv6 packet (we don't need the actual // header, but want to detect parsing errors) _, err := ipv6.ParseHeader(body.Data) if err != nil { return } bodyData = body.Data[ipv6.HeaderLen:] default: return } // parse ICMP message after the IP header msg, err := icmp.ParseMessage(proto, bodyData) if err != nil { return } pinger.process(msg.Body, fmt.Errorf("%s", m.Type), nil, nil) } } // process will finish a currently running Echo Request, if the body is // an ICMP Echo reply to a request from us. func (pinger *Pinger) process(body icmp.MessageBody, result error, addr net.IP, tRecv *time.Time) { echo, ok := body.(*icmp.Echo) if !ok || echo == nil { if pinger.LogUnexpectedPackets { log.Infof("expected *icmp.Echo, got %#v", body) } return } idseq := (uint32(uint16(echo.ID)) << 16) | uint32(uint16(echo.Seq)) // search for existing running echo request pinger.mtx.Lock() req := pinger.requests[idseq] if _, ok := req.(*simpleRequest); ok { // a simpleRequest is finished on the first reply delete(pinger.requests, idseq) } pinger.mtx.Unlock() if req != nil { req.handleReply(result, addr, tRecv) } } ================================================ FILE: request.go ================================================ package ping import ( "net" "sync" "time" ) type request interface { init() close() handleReply(error, net.IP, *time.Time) } // A multiRequest is a currently running ICMP echo request waiting for multple answers. type multiRequest struct { tStart time.Time // when was the request packet sent? replies chan Reply closed bool mtx sync.RWMutex } // Reply is a reply to a multicast echo request type Reply struct { Address net.IP Duration time.Duration } // A simpleRequest is a currently running ICMP echo request waiting for a single answer. type simpleRequest struct { wait chan struct{} result error tStart time.Time // when was this packet sent? tFinish *time.Time // if and when was the reply received? } // handleReply is responsible for finishing this request. // It takes an error as failure reason. func (req *simpleRequest) handleReply(err error, _ net.IP, tRecv *time.Time) { req.result = err // update tFinish only if no error present and value wasn't previously set if err == nil && tRecv != nil && req.tFinish == nil { req.tFinish = tRecv } req.close() } func (req *simpleRequest) init() { req.wait = make(chan struct{}) req.tStart = time.Now() } func (req *simpleRequest) close() { defer func() { // Double-closing is very unlikely, but a race condition may // happen when sending fails and a reply is received anyway. recover() }() close(req.wait) } func (req *simpleRequest) roundTripTime() (time.Duration, error) { if req.result != nil { return 0, req.result } if req.tFinish == nil { return 0, nil } return req.tFinish.Sub(req.tStart), nil } func (req *multiRequest) init() { req.replies = make(chan Reply) req.tStart = time.Now() } func (req *multiRequest) close() { req.mtx.Lock() req.closed = true close(req.replies) req.mtx.Unlock() } // handleReply is responsible for adding a result to the result set func (req *multiRequest) handleReply(err error, addr net.IP, tRecv *time.Time) { if err != nil { return } // avoid blocking go func() { req.mtx.RLock() defer req.mtx.RUnlock() if !req.closed { req.replies <- Reply{ Address: addr, Duration: tRecv.Sub(req.tStart), } } }() } ================================================ FILE: sending.go ================================================ package ping import ( "context" "errors" "net" "sync" "sync/atomic" "time" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" ) // PingAttempts sends ICMP echo requests with a timeout per request, retrying upto `attempt` times . // Will finish early on success and return the round trip time of the last ping. func (pinger *Pinger) PingAttempts(destination *net.IPAddr, timeout time.Duration, attempts int) (rtt time.Duration, err error) { if attempts < 1 { err = errors.New("zero attempts") } else { for i := 0; i < attempts; i++ { rtt, err = pinger.Ping(destination, timeout) if err == nil { break // success } } } return } // Ping sends a single Echo Request and waits for an answer. It returns // the round trip time (RTT) if a reply is received in time. func (pinger *Pinger) Ping(destination *net.IPAddr, timeout time.Duration) (time.Duration, error) { ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(timeout)) defer cancel() return pinger.PingContext(ctx, destination) } // PingContext sends a single Echo Request and waits for an answer. It returns // the round trip time (RTT) if a reply is received before cancellation of the context. func (pinger *Pinger) PingContext(ctx context.Context, destination *net.IPAddr) (time.Duration, error) { req := simpleRequest{} idseq, err := pinger.sendRequest(destination, &req) if err != nil { return 0, err } // wait for answer select { case <-req.wait: // already dequeued err = req.result case <-ctx.Done(): // dequeue request pinger.removeRequest(idseq) err = &timeoutError{} } if err != nil { return 0, err } return req.roundTripTime() } // PingMulticast sends a single echo request and returns a channel for the responses. // The channel will be closed on termination of the context. // An error is returned if the sending of the echo request fails. func (pinger *Pinger) PingMulticast(destination *net.IPAddr, wait time.Duration) (<-chan Reply, error) { ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(wait)) defer cancel() return pinger.PingMulticastContext(ctx, destination) } // PingMulticastContext does the same as PingMulticast but receives a context func (pinger *Pinger) PingMulticastContext(ctx context.Context, destination *net.IPAddr) (<-chan Reply, error) { req := multiRequest{} idseq, err := pinger.sendRequest(destination, &req) if err != nil { return nil, err } go func() { <-ctx.Done() // dequeue request pinger.removeRequest(idseq) req.close() }() return req.replies, nil } // sendRequest marshals the payload and sends the packet. // It returns the combined id+sequence number and an error if the sending failed. func (pinger *Pinger) sendRequest(destination *net.IPAddr, req request) (uint32, error) { id := uint16(pinger.Id) seq := uint16(atomic.AddUint32(pinger.SequenceCounter, 1)) idseq := (uint32(id) << 16) | uint32(seq) pinger.payloadMu.RLock() defer pinger.payloadMu.RUnlock() // build packet wm := icmp.Message{ Code: 0, Body: &icmp.Echo{ ID: int(id), Seq: int(seq), Data: pinger.payload, }, } // Protocol specifics var conn net.PacketConn var lock *sync.Mutex if destination.IP.To4() != nil { wm.Type = ipv4.ICMPTypeEcho conn = pinger.conn4 lock = &pinger.write4 } else { wm.Type = ipv6.ICMPTypeEchoRequest conn = pinger.conn6 lock = &pinger.write6 } // serialize packet wb, err := wm.Marshal(nil) if err != nil { return idseq, err } // enqueue in currently running requests pinger.mtx.Lock() pinger.requests[idseq] = req pinger.mtx.Unlock() // start measurement (tStop is set in the receiving end) lock.Lock() req.init() // send request _, err = conn.WriteTo(wb, destination) lock.Unlock() // send failed, need to remove request from list if err != nil { req.close() pinger.removeRequest(idseq) return idseq, err } return idseq, nil }