[
  {
    "path": ".codecov.yml",
    "content": "ignore:\n- cmd\n"
  },
  {
    "path": ".github/build-all",
    "content": "#!/bin/sh -e\n\nfor mf in $(find cmd -name Makefile); do\n  make -C \"$(dirname $mf)\"\ndone\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: build\n\non:\n- push\n- pull_request\n\njobs:\n  build:\n    name: Tests\n    runs-on: ubuntu-latest\n    steps:\n\n    - name: Set up Go 1.x\n      uses: actions/setup-go@v5\n      with:\n        go-version: ^1.23\n      id: go\n\n    - name: Check out code into the Go module directory\n      uses: actions/checkout@v5\n\n    - name: Build commands\n      run: .github/build-all\n\n    - name: Run tests\n      run: sudo go test -v -coverprofile coverage.txt ./...\n\n    - name: Upload coverage report\n      uses: codecov/codecov-action@v5\n      with:\n        files: coverage.txt\n"
  },
  {
    "path": ".github/workflows/golangci-lint.yml",
    "content": "name: golangci-lint\n\non:\n- push\n- pull_request\n\njobs:\n  golangci:\n    name: lint\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v5\n      - uses: actions/setup-go@v5\n        with:\n          go-version: ^1.23\n      - name: golangci-lint\n        uses: golangci/golangci-lint-action@v8\n        with:\n          version: v2.4.0\n"
  },
  {
    "path": ".gitignore",
    "content": "# Binaries for programs and plugins\n*.exe\n*.dll\n*.so\n*.dylib\n\n# Test binary, build with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736\n.glide/\n\n# Build fragments\ncmd/multiping/multiping\ncmd/multiping/multiping.log\ncmd/ping-test/ping-test\ncmd/ping-test/ping-test.log\ncmd/ping-monitor/ping-monitor\ncmd/pingnet/pingnet\n"
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\nlinters:\n  exclusions:\n    generated: lax\n    presets:\n      - comments\n      - common-false-positives\n      - legacy\n      - std-error-handling\n    rules:\n      - linters:\n          - errcheck\n        path: \\.go\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\nformatters:\n  exclusions:\n    generated: lax\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 Digineo GmbH\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# go-ping\n\n[![GoDoc](https://godoc.org/github.com/digineo/go-ping?status.svg)](https://godoc.org/github.com/digineo/go-ping)\n[![Build Status](https://github.com/digineo/go-ping/workflows/build/badge.svg?branch=master)](https://github.com/digineo/go-ping/actions)\n[![Codecov](https://codecov.io/gh/digineo/go-ping/branch/master/graph/badge.svg)](https://codecov.io/gh/digineo/go-ping)\n[![Go Report Card](https://goreportcard.com/badge/github.com/digineo/go-ping)](https://goreportcard.com/report/github.com/digineo/go-ping)\n\nA simple ICMP Echo implementation, based on [golang.org/x/net/icmp][net-icmp].\n\nSome sample programs are provided in `cmd/`:\n\n- [**`ping-test`**][ping-test] is a really simple ping clone\n- [**`multiping`**][multiping] provides an interactive TUI to ping multiple hosts\n- [**`ping-monitor`**][monitor] pings multiple hosts in parallel, but just prints the summary every so often\n- [**`pingnet`**][pingnet] allows to ping every host in a CIDR range (e.g. 0.0.0.0/0 :-))\n\n[net-icmp]: https://godoc.org/golang.org/x/net/icmp\n[ping-test]: https://github.com/digineo/go-ping/tree/master/cmd/ping-test\n[multiping]: https://github.com/digineo/go-ping/tree/master/cmd/multiping\n[monitor]: https://github.com/digineo/go-ping/tree/master/cmd/ping-monitor\n[pingnet]: https://github.com/digineo/go-ping/tree/master/cmd/pingnet\n\n## Features\n\n- [x] IPv4 and IPv6 support\n- [x] Unicast and multicast support\n- [x] configurable retry amount and timeout duration\n- [x] configurable payload size (and content)\n- [x] round trip time measurement\n\n## Contribute\n\nSimply fork and create a pull-request. We'll try to respond in a timely\nfashion.\n\n## Software using this library\n\n* [Ping Exporter for Prometheus](https://github.com/czerwonk/ping_exporter)\n\nPlease create a pull request to get your software listed.\n\n## License\n\nMIT License, Copyright (c) 2018 Digineo GmbH\n\n<https://www.digineo.de>\n"
  },
  {
    "path": "cmd/common.mk",
    "content": ".PHONY: all\nall: $(TARGET)\n\n$(TARGET):\n\tgo build -o $@\n\n.PHONY: clean\nclean:\n\trm -f $(TARGET) $(TARGET).log\n"
  },
  {
    "path": "cmd/multiping/Makefile",
    "content": "TARGET = multiping\n\ninclude ../common.mk\n\n.PHONY: test\ntest: all\n\tgo test ./...\n\t./$(TARGET) golang.org cloudflare.com 2>$(TARGET).log\n\tcat $(TARGET).log\n"
  },
  {
    "path": "cmd/multiping/README.md",
    "content": "# multiping\n\nJust like regular `ping`, but measures round trip time to multiple\nhosts, all at once.\n\nThe user interface is similar to [mtr](https://www.bitwizard.nl/mtr/):\n\n![Screenshot of multiping in action](multiping.png)\n\n\n## Installation\n\nInstalling is as easy as:\n\n```\n$ go get -u github.com/digineo/go-ping/cmd/multiping\n```\n\nThis will download and build this program and install it into `$GOPATH/bin/`\n(assuming you have the Go toolchain installed).\n\nTo run it, you need elevated privileges (as mentioned in the\n[README of ping-test](../ping-test)). You can either run it as root (or\nvia `sudo`; both not recommended), or enable the program to only open\nraw sockets (via `setcap`, but Linux-only):\n\n```\n$ sudo setcap cap_net_raw+ep $GOPATH/bin/multiping\n```\n\n## Running\n\nAssuming `$GOPATH/bin` is in your `$PATH`, just call it with a list\nof hosts and IP addresses (currently only IPv4):\n\n```\n$ multiping golang.org google.com 127.0.0.1\n```\n\n### Options\n\nTo get a list of available options, run `multiping -h`:\n\n```\nUsage of ./multiping:\n  -bind4 string\n    \tIPv4 bind address (default \"0.0.0.0\")\n  -bind6 string\n    \tIPv6 bind address (default \"::\")\n  -buf uint\n    \tbuffer size for statistics (default 50)\n  -interval duration\n    \tpolling interval (default 1s)\n  -resolve duration\n    \ttimeout for DNS lookups (default 1.5s)\n  -s uint\n    \tsize of payload in bytes (default 56)\n  -timeout duration\n    \ttimeout for a single echo request (default 1s)\n```\n\n## Roadmap\n\n- [x] cleanup UI code (this is a bit of a mess)\n- [ ] add more features\n  - [ ] different display modes (`mtr` has different views)\n  - [x] move \"last error\" column into a log area at the bottom\n  - [ ] increase/decrease interval and/or timeout with `-`/`+` keys\n- [x] fill IPv6 options with life (once the library has IPv6 support)\n- [x] use something more sophisticated than `net.ResolveIPAddress` to\n  get a list of all A/AAAA records for a given domain name\n  - [ ] this propably needs an off-switch\n"
  },
  {
    "path": "cmd/multiping/destination.go",
    "content": "package main\n\nimport (\n\t\"log\"\n\t\"math\"\n\t\"net\"\n\t\"sync\"\n\t\"time\"\n\n\tping \"github.com/digineo/go-ping\"\n)\n\ntype history struct {\n\treceived int\n\tlost     int\n\tresults  []time.Duration // ring, start index = .received%len\n\tmtx      sync.RWMutex\n}\n\ntype destination struct {\n\thost   string\n\tremote *net.IPAddr\n\t*history\n}\n\ntype stat struct {\n\tpktSent int\n\tpktLoss float64\n\tlast    time.Duration\n\tbest    time.Duration\n\tworst   time.Duration\n\tmean    time.Duration\n\tstddev  time.Duration\n}\n\nfunc (u *destination) ping(pinger *ping.Pinger) {\n\trtt, err := pinger.Ping(u.remote, opts.timeout)\n\tif err != nil {\n\t\tlog.Printf(\"[yellow]%s[white]: %v\", u.host, err)\n\t}\n\tu.addResult(rtt, err)\n}\n\nfunc (s *history) addResult(rtt time.Duration, err error) {\n\ts.mtx.Lock()\n\tif err == nil {\n\t\ts.results[s.received%len(s.results)] = rtt\n\t\ts.received++\n\t} else {\n\t\ts.lost++\n\t}\n\ts.mtx.Unlock()\n}\n\nfunc (s *history) compute() (st stat) {\n\ts.mtx.RLock()\n\tdefer s.mtx.RUnlock()\n\n\tif s.received == 0 {\n\t\tif s.lost > 0 {\n\t\t\tst.pktLoss = 1.0\n\t\t}\n\t\treturn\n\t}\n\n\tcollection := s.results[:]\n\tst.pktSent = s.received + s.lost\n\tsize := len(s.results)\n\tst.last = collection[(s.received-1)%size]\n\n\t// we don't yet have filled the buffer\n\tif s.received <= size {\n\t\tcollection = s.results[:s.received]\n\t\tsize = s.received\n\t}\n\n\tst.pktLoss = float64(s.lost) / float64(s.received+s.lost)\n\tst.best, st.worst = collection[0], collection[0]\n\n\ttotal := time.Duration(0)\n\tfor _, rtt := range collection {\n\t\tif rtt < st.best {\n\t\t\tst.best = rtt\n\t\t}\n\t\tif rtt > st.worst {\n\t\t\tst.worst = rtt\n\t\t}\n\t\ttotal += rtt\n\t}\n\n\tst.mean = time.Duration(float64(total) / float64(size))\n\n\tstddevNum := float64(0)\n\tfor _, rtt := range collection {\n\t\tdev := float64(rtt - st.mean)\n\t\tstddevNum += dev * dev\n\t}\n\tst.stddev = time.Duration(math.Sqrt(stddevNum / float64(size)))\n\n\treturn\n}\n"
  },
  {
    "path": "cmd/multiping/destination_test.go",
    "content": "package main\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestComputeStats(t *testing.T) {\n\tassert := assert.New(t)\n\tconst (\n\t\tz  = time.Duration(0)\n\t\tms = time.Millisecond\n\t\tµs = time.Microsecond\n\t\tns = time.Nanosecond\n\t)\n\n\ttestcases := []struct {\n\t\ttitle    string\n\t\tresults  []time.Duration\n\t\treceived int\n\t\tlost     int\n\n\t\tlast   time.Duration\n\t\tbest   time.Duration\n\t\tworst  time.Duration\n\t\tmean   time.Duration\n\t\tstddev time.Duration\n\t\tloss   float64\n\t}{\n\t\t{\n\t\t\ttitle:    \"simplest case\",\n\t\t\tresults:  []time.Duration{},\n\t\t\treceived: 0,\n\t\t\tlast:     z, best: z, worst: z, mean: z, stddev: z,\n\t\t},\n\t\t{\n\t\t\ttitle:    \"another simple case\",\n\t\t\tresults:  []time.Duration{ms},\n\t\t\treceived: 1,\n\t\t\tlast:     ms, best: ms, worst: ms, mean: ms, stddev: z,\n\t\t},\n\t\t{\n\t\t\ttitle:    \"same as before, but sent>len(res)\",\n\t\t\tresults:  []time.Duration{ms},\n\t\t\treceived: 3,\n\t\t\tlast:     ms, best: ms, worst: ms, mean: ms, stddev: z,\n\t\t},\n\t\t{\n\t\t\ttitle:    \"same as before, but sent<len(res)\",\n\t\t\tresults:  []time.Duration{ms, ms, 5 * ms},\n\t\t\treceived: 2,\n\t\t\tlast:     ms, best: ms, worst: ms, mean: ms, stddev: z,\n\t\t},\n\t\t{\n\t\t\ttitle:    \"different numbers, manually calculated\",\n\t\t\tresults:  []time.Duration{ms, 2 * ms},\n\t\t\treceived: 2,\n\t\t\tlast:     2 * ms,\n\t\t\tbest:     ms,\n\t\t\tworst:    2 * ms,\n\t\t\tmean:     1500 * µs,\n\t\t\tstddev:   500 * µs,\n\t\t},\n\t\t{\n\t\t\ttitle:    \"wilder numbers\",\n\t\t\tresults:  []time.Duration{6 * ms, 2 * ms, 14 * ms, 11 * ms},\n\t\t\treceived: 6,\n\t\t\tlost:     2,\n\t\t\tlast:     2 * ms, // res[received%len]\n\t\t\tbest:     2 * ms,\n\t\t\tworst:    14 * ms,\n\t\t\tmean:     8250 * µs, // (6000+2000+14000+11000)/4\n\t\t\tstddev:   4602988,   // 4602988.15988\n\t\t\tloss:     0.25,      // sent = 6+2\n\t\t},\n\t\t{\n\t\t\ttitle:    \"verifying captured data\",\n\t\t\treceived: 50,\n\t\t\tlost:     7,\n\t\t\tloss:     0.1228, // 7 / 57\n\n\t\t\tlast:   488619758,\n\t\t\tbest:   451327200,\n\t\t\tworst:  492082650,\n\t\t\tmean:   487287379,\n\t\t\tstddev: 9356133,\n\n\t\t\tresults: []time.Duration{\n\t\t\t\t478427841, 486727913, 489902185, 490369676, 489957386,\n\t\t\t\t490784152, 491390728, 491012043, 491313203, 489869560,\n\t\t\t\t488634310, 451590351, 480933928, 451431418, 491046095,\n\t\t\t\t492017348, 488906398, 490187284, 490733777, 490418928,\n\t\t\t\t490627269, 490710944, 491339118, 491300740, 490320794,\n\t\t\t\t489706066, 487735713, 488153523, 490988560, 490293234,\n\t\t\t\t492082650, 490784586, 488731408, 488008147, 487630508,\n\t\t\t\t490190288, 490712289, 489931645, 490608008, 490625639,\n\t\t\t\t491721463, 451327200, 491615584, 490238328, 489234608,\n\t\t\t\t488510694, 488807517, 489176334, 488981822, 488619758,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i, tc := range testcases {\n\t\th := history{received: tc.received, results: tc.results, lost: tc.lost}\n\t\tsubject := h.compute()\n\n\t\tassert.Equal(tc.best, subject.best, \"test case #%d (%s): best\", i, tc.title)\n\t\tassert.Equal(tc.last, subject.last, \"test case #%d (%s): last\", i, tc.title)\n\t\tassert.Equal(tc.worst, subject.worst, \"test case #%d (%s): worst\", i, tc.title)\n\t\tassert.Equal(tc.mean, subject.mean, \"test case #%d (%s): mean\", i, tc.title)\n\t\tassert.Equal(tc.stddev, subject.stddev, \"test case #%d (%s): stddev\", i, tc.title)\n\t\tassert.Equal(tc.received+tc.lost, subject.pktSent, \"test case #%d (%s): pktSent\", i, tc.title)\n\t\tassert.InDelta(tc.loss, subject.pktLoss, 0.0001, \"test case #%d (%s): pktLoss\", i, tc.title)\n\t}\n}\n"
  },
  {
    "path": "cmd/multiping/main.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"time\"\n\n\tping \"github.com/digineo/go-ping\"\n)\n\nvar opts = struct {\n\ttimeout         time.Duration\n\tinterval        time.Duration\n\tpayloadSize     uint\n\tstatBufferSize  uint\n\tbind4           string\n\tbind6           string\n\tdests           []*destination\n\tresolverTimeout time.Duration\n}{\n\ttimeout:         1000 * time.Millisecond,\n\tinterval:        1000 * time.Millisecond,\n\tbind4:           \"0.0.0.0\",\n\tbind6:           \"::\",\n\tpayloadSize:     56,\n\tstatBufferSize:  50,\n\tresolverTimeout: 1500 * time.Millisecond,\n}\n\nvar (\n\tpinger *ping.Pinger\n\ttui    *userInterface\n)\n\nfunc main() {\n\tflag.Usage = func() {\n\t\tfmt.Fprintln(os.Stderr, \"Usage:\", os.Args[0], \"[options] host [host [...]]\")\n\t\tflag.PrintDefaults()\n\t}\n\n\tflag.DurationVar(&opts.timeout, \"timeout\", opts.timeout, \"timeout for a single echo request\")\n\tflag.DurationVar(&opts.interval, \"interval\", opts.interval, \"polling interval\")\n\tflag.UintVar(&opts.payloadSize, \"s\", opts.payloadSize, \"size of payload in bytes\")\n\tflag.UintVar(&opts.statBufferSize, \"buf\", opts.statBufferSize, \"buffer size for statistics\")\n\tflag.StringVar(&opts.bind4, \"bind4\", opts.bind4, \"IPv4 bind address\")\n\tflag.StringVar(&opts.bind6, \"bind6\", opts.bind6, \"IPv6 bind address\")\n\tflag.DurationVar(&opts.resolverTimeout, \"resolve\", opts.resolverTimeout, \"timeout for DNS lookups\")\n\tflag.Parse()\n\n\tlog.SetFlags(0)\n\n\tfor _, host := range flag.Args() {\n\t\tremotes, err := resolve(host, opts.resolverTimeout)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"error resolving host %s: %v\", host, err)\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, remote := range remotes {\n\t\t\tif v4 := remote.IP.To4() != nil; v4 && opts.bind4 == \"\" || !v4 && opts.bind6 == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tipaddr := remote // need to create a copy\n\t\t\tdst := destination{\n\t\t\t\thost:   host,\n\t\t\t\tremote: &ipaddr,\n\t\t\t\thistory: &history{\n\t\t\t\t\tresults: make([]time.Duration, opts.statBufferSize),\n\t\t\t\t},\n\t\t\t}\n\n\t\t\topts.dests = append(opts.dests, &dst)\n\t\t}\n\t}\n\n\tif instance, err := ping.New(opts.bind4, opts.bind6); err == nil {\n\t\tif instance.PayloadSize() != uint16(opts.payloadSize) {\n\t\t\tinstance.SetPayloadSize(uint16(opts.payloadSize))\n\t\t}\n\t\tpinger = instance\n\t\tdefer pinger.Close()\n\t} else {\n\t\tpanic(err)\n\t}\n\n\tgo work()\n\n\ttui = buildTUI(opts.dests)\n\tgo tui.update(time.Second)\n\n\tif err := tui.Run(); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc work() {\n\tfor {\n\t\tfor i, u := range opts.dests {\n\t\t\tgo func(u *destination, i int) {\n\t\t\t\tu.ping(pinger)\n\t\t\t}(u, i)\n\t\t}\n\t\ttime.Sleep(opts.interval)\n\t}\n}\n"
  },
  {
    "path": "cmd/multiping/resolve.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"strings\"\n\t\"time\"\n)\n\nfunc resolve(addr string, timeout time.Duration) ([]net.IPAddr, error) {\n\tif strings.ContainsRune(addr, '%') {\n\t\tipaddr, err := net.ResolveIPAddr(\"ip\", addr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn []net.IPAddr{*ipaddr}, nil\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\tdefer cancel()\n\n\treturn net.DefaultResolver.LookupIPAddr(ctx, addr)\n}\n"
  },
  {
    "path": "cmd/multiping/ui.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/rivo/tview\"\n)\n\ntype userInterface struct {\n\tapp          *tview.Application\n\tgrid         *tview.Grid\n\ttable        *tview.Table\n\tdestinations []*destination\n}\n\nvar coldef = [...]struct {\n\ttitle   string\n\talign   int\n\tinitVal func(*destination) string\n\tcontent func(*stat) string\n}{\n\t{\n\t\ttitle:   \"host\",\n\t\talign:   tview.AlignLeft,\n\t\tinitVal: func(d *destination) string { return d.host },\n\t},\n\t{\n\t\ttitle:   \"address\",\n\t\talign:   tview.AlignLeft,\n\t\tinitVal: func(d *destination) string { return d.remote.IP.String() },\n\t},\n\t{\n\t\ttitle:   \"sent\",\n\t\talign:   tview.AlignRight,\n\t\tcontent: func(st *stat) string { return strconv.Itoa(st.pktSent) },\n\t},\n\t{\n\t\ttitle:   \"loss\",\n\t\talign:   tview.AlignRight,\n\t\tcontent: func(st *stat) string { return fmt.Sprintf(\"%0.1f%%\", st.pktLoss*100) },\n\t},\n\t{\n\t\ttitle:   \"last\",\n\t\talign:   tview.AlignRight,\n\t\tcontent: func(st *stat) string { return ts(st.last) },\n\t},\n\t{\n\t\ttitle:   \"best\",\n\t\talign:   tview.AlignRight,\n\t\tcontent: func(st *stat) string { return ts(st.best) },\n\t},\n\t{\n\t\ttitle:   \"worst\",\n\t\talign:   tview.AlignRight,\n\t\tcontent: func(st *stat) string { return ts(st.worst) },\n\t},\n\t{\n\t\ttitle:   \"mean\",\n\t\talign:   tview.AlignRight,\n\t\tcontent: func(st *stat) string { return ts(st.mean) },\n\t},\n\t{\n\t\ttitle:   \"stddev\",\n\t\talign:   tview.AlignRight,\n\t\tcontent: func(st *stat) string { return ts(st.stddev) },\n\t},\n}\n\nfunc buildTUI(destinations []*destination) *userInterface {\n\tui := &userInterface{\n\t\tapp:          tview.NewApplication(),\n\t\ttable:        tview.NewTable().SetBorders(false).SetFixed(2, 0),\n\t\tgrid:         tview.NewGrid().SetRows(3, 0, 10).SetColumns(0),\n\t\tdestinations: destinations,\n\t}\n\n\ttitle := tview.NewTextView().\n\t\tSetDynamicColors(true).\n\t\tSetTextAlign(tview.AlignCenter).\n\t\tSetText(\"[yellow]multiping[white]   press q to exit\")\n\n\tlogs := tview.NewTextView().\n\t\tSetDynamicColors(true).\n\t\tSetWrap(false)\n\tlog.SetFlags(log.Ltime | log.LUTC)\n\tlog.SetOutput(logs)\n\n\tui.grid.AddItem(title, 0, 0, 1, 1, 0, 0, false)\n\tui.grid.AddItem(ui.table, 1, 0, 1, 1, 0, 0, true)\n\tui.grid.AddItem(logs, 2, 0, 1, 1, 0, 0, false)\n\n\t// setup controls\n\tui.app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {\n\t\tswitch event.Key() {\n\t\tcase tcell.KeyEscape, tcell.KeyCtrlC:\n\t\t\tui.app.Stop()\n\t\t\treturn nil\n\t\tcase tcell.KeyRune:\n\t\t\tif event.Rune() == 'q' {\n\t\t\t\tui.app.Stop()\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\treturn event\n\t})\n\n\t// build header\n\tfor col, def := range coldef {\n\t\tcell := tview.NewTableCell(def.title).SetAlign(def.align)\n\t\tif col == 2 {\n\t\t\tcell.SetExpansion(1)\n\t\t}\n\t\tui.table.SetCell(0, col, cell)\n\t}\n\n\t// prepare data list\n\tfor r, dst := range destinations {\n\t\tfor c, def := range coldef {\n\t\t\tvar cell *tview.TableCell\n\t\t\tif def.initVal != nil {\n\t\t\t\tcell = tview.NewTableCell(def.initVal(dst))\n\t\t\t} else {\n\t\t\t\tcell = tview.NewTableCell(\"n/a\")\n\t\t\t}\n\t\t\tui.table.SetCell(r+2, c, cell.SetAlign(def.align))\n\t\t}\n\t}\n\n\treturn ui\n}\n\nfunc (ui *userInterface) Run() error {\n\tui.app.SetRoot(ui.grid, true).SetFocus(ui.table)\n\treturn ui.app.Run()\n}\n\nfunc (ui *userInterface) update(interval time.Duration) {\n\ttime.Sleep(interval)\n\tfor {\n\t\tfor i, u := range ui.destinations {\n\t\t\tstats := u.compute()\n\t\t\tr := i + 2\n\n\t\t\tfor col, def := range coldef {\n\t\t\t\tif def.content != nil {\n\t\t\t\t\tui.table.GetCell(r, col).SetText(def.content(&stats))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tui.app.Draw()\n\t\ttime.Sleep(interval)\n\t}\n}\n\nconst tsDividend = float64(time.Millisecond) / float64(time.Nanosecond)\n\nfunc ts(dur time.Duration) string {\n\tif 10*time.Microsecond < dur && dur < time.Second {\n\t\treturn fmt.Sprintf(\"%0.2f\", float64(dur.Nanoseconds())/tsDividend)\n\t}\n\treturn dur.String()\n}\n"
  },
  {
    "path": "cmd/ping-monitor/Makefile",
    "content": "TARGET = ping-monitor\n\ninclude ../common.mk\n"
  },
  {
    "path": "cmd/ping-monitor/main.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/digineo/go-ping\"\n\t\"github.com/digineo/go-ping/monitor\"\n)\n\nvar (\n\tpingInterval        = 5 * time.Second\n\tpingTimeout         = 4 * time.Second\n\treportInterval      = 60 * time.Second\n\tsize           uint = 56\n\tpinger         *ping.Pinger\n\ttargets        []string\n)\n\nfunc main() {\n\tflag.Usage = func() {\n\t\tfmt.Fprintln(os.Stderr, \"Usage:\", os.Args[0], \"[options] host [host [...]]\")\n\t\tflag.PrintDefaults()\n\t}\n\n\tflag.DurationVar(&pingInterval, \"pingInterval\", pingInterval, \"interval for ICMP echo requests\")\n\tflag.DurationVar(&pingTimeout, \"pingTimeout\", pingTimeout, \"timeout for ICMP echo request\")\n\tflag.DurationVar(&reportInterval, \"reportInterval\", reportInterval, \"interval for reports\")\n\tflag.UintVar(&size, \"size\", size, \"size of additional payload data\")\n\tflag.Parse()\n\n\tif n := flag.NArg(); n == 0 {\n\t\t// Targets empty?\n\t\tflag.Usage()\n\t\tos.Exit(1)\n\t} else if n > int(^byte(0)) {\n\t\t// Too many targets?\n\t\tfmt.Println(\"Too many targets\")\n\t\tos.Exit(1)\n\t}\n\n\t// Bind to sockets\n\tif p, err := ping.New(\"0.0.0.0\", \"::\"); err != nil {\n\t\tfmt.Printf(\"Unable to bind: %s\\nRunning as root?\\n\", err)\n\t\tos.Exit(2)\n\t} else {\n\t\tpinger = p\n\t}\n\tpinger.SetPayloadSize(uint16(size))\n\tdefer pinger.Close()\n\n\t// Create monitor\n\tmonitor := monitor.New(pinger, pingInterval, pingTimeout)\n\tdefer monitor.Stop()\n\n\t// Add targets\n\ttargets = flag.Args()\n\tfor i, target := range targets {\n\t\tipAddr, err := net.ResolveIPAddr(\"\", target)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"invalid target '%s': %s\", target, err)\n\t\t\tcontinue\n\t\t}\n\t\tmonitor.AddTargetDelayed(string([]byte{byte(i)}), *ipAddr, 10*time.Millisecond*time.Duration(i))\n\t}\n\n\t// Start report routine\n\tticker := time.NewTicker(reportInterval)\n\tdefer ticker.Stop()\n\tgo func() {\n\t\tfor range ticker.C {\n\t\t\tfor i, metrics := range monitor.ExportAndClear() {\n\t\t\t\tfmt.Printf(\"%s: %+v\\n\", targets[[]byte(i)[0]], *metrics)\n\t\t\t}\n\t\t}\n\t}()\n\n\t// Handle SIGINT and SIGTERM.\n\tch := make(chan os.Signal, 1)\n\tsignal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)\n\tfmt.Println(\"received\", <-ch)\n}\n"
  },
  {
    "path": "cmd/ping-test/Makefile",
    "content": "TARGET = ping-test\n\ninclude ../common.mk\n\n.PHONY: test\ntest: all\n\t@truncate -s0 $(TARGET).log\n\t@./$(TARGET) -4 golang.org      2>>$(TARGET).log\n\t@./$(TARGET) -6 golang.org      2>>$(TARGET).log\n\t@./$(TARGET) -4 cloudflare.com  2>>$(TARGET).log\n\t@./$(TARGET) -6 cloudflare.com  2>>$(TARGET).log\n\n\t@echo error log:\n\t@cat $(TARGET).log\n"
  },
  {
    "path": "cmd/ping-test/README.md",
    "content": "# ping-test\n\nThis is a sample program to demonstrate the usage of this library.\nIt is not intended for production use.\n\n## How-to\n\n**Building/installing** does not require any special steps. Either run\n`go get` (to install it directly in `$GOPATH/bin/ping-test`)\n\n```\n$ go get -u github.com/digineo/go-ping/cmd/ping-test\n```\n\nor skip installing it (and build it yourself):\n\n```\n$ go get -u -d github.com/digineo/go-ping\n$ cd $GOPATH/src/github.com/digineo/go-ping/cmd/ping-test\n$ go build    # this creates ./ping-test\n```\n\n**Running** `ping-test` requires elevated privileges, since normal users\ncannot open ICMP sockets.\n\nTo circumvent this, either run the binary as root, e.g. via `sudo`\n *(not recommended!)*\n\n```\n$ sudo ./ping-test -4 golang.org\nping golang.org (216.58.211.113) rtt=11.869403ms\n$ sudo ./ping-test -6 golang.org\nping golang.org (2a00:1450:400e:809::2011) rtt=11.412907ms\n```\n\nBetter yet, allow the binary to only open raw sockets (via `capabilities(7)`):\n\n```\n$ sudo setcap cap_net_raw+ep ./ping-test\n$ ./ping-test -4 golang.org\nping golang.org (216.58.211.113) rtt=11.772573ms\n$ ./ping-test -6 golang.org\nping golang.org (2a00:1450:400e:809::2011) rtt=11.31439ms\n```\n\nNote, that you'll need to re-apply the `setcap` command everytime the\nbinary changes (i.e. after `go build`).\n\nAlso, since configuring the system capabilities is a Linux feature, you\nmay need to resort to Docker or VM environments, if you want to try\nthis binary, but don't trust its source code. Or you like living in the\ndanger zone and don't mind the occasional system crash introduced by\nrunning code found on the internet with root privileges :-)\n"
  },
  {
    "path": "cmd/ping-test/main.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"os\"\n\t\"time\"\n\n\tping \"github.com/digineo/go-ping\"\n)\n\nvar (\n\targs           []string\n\tattempts       uint = 3\n\ttimeout             = time.Second\n\tproto4, proto6 bool\n\tsize           uint = 56\n\tbind           string\n\n\tdestination string\n\tremoteAddr  *net.IPAddr\n\tpinger      *ping.Pinger\n)\n\nfunc main() {\n\tflag.Usage = func() {\n\t\tfmt.Fprintln(os.Stderr, \"Usage:\", os.Args[0], \"[options] host [host [...]]\")\n\t\tflag.PrintDefaults()\n\t}\n\n\tflag.UintVar(&attempts, \"attempts\", attempts, \"number of attempts\")\n\tflag.DurationVar(&timeout, \"timeout\", timeout, \"timeout for a single echo request\")\n\tflag.UintVar(&size, \"s\", size, \"size of additional payload data\")\n\tflag.BoolVar(&proto4, \"4\", proto4, \"use IPv4 (mutually exclusive with -6)\")\n\tflag.BoolVar(&proto6, \"6\", proto6, \"use IPv6 (mutually exclusive with -4)\")\n\tflag.StringVar(&bind, \"bind\", \"\", \"IPv4 or IPv6 bind address (defaults to 0.0.0.0 for IPv4 and :: for IPv6)\")\n\tflag.Parse()\n\n\tif proto4 == proto6 {\n\t\tlog.Fatalf(\"need exactly one of -4 and -6 flags\")\n\t}\n\n\tif bind == \"\" {\n\t\tif proto4 {\n\t\t\tbind = \"0.0.0.0\"\n\t\t} else if proto6 {\n\t\t\tbind = \"::\"\n\t\t}\n\t}\n\n\targs := flag.Args()\n\tdestination := args[0]\n\n\tif proto4 {\n\t\tif r, err := net.ResolveIPAddr(\"ip4\", destination); err != nil {\n\t\t\tpanic(err)\n\t\t} else {\n\t\t\tremoteAddr = r\n\t\t}\n\n\t\tif p, err := ping.New(bind, \"\"); err != nil {\n\t\t\tpanic(err)\n\t\t} else {\n\t\t\tpinger = p\n\t\t}\n\t} else if proto6 {\n\t\tif r, err := net.ResolveIPAddr(\"ip6\", destination); err != nil {\n\t\t\tpanic(err)\n\t\t} else {\n\t\t\tremoteAddr = r\n\t\t}\n\n\t\tif p, err := ping.New(\"\", bind); err != nil {\n\t\t\tpanic(err)\n\t\t} else {\n\t\t\tpinger = p\n\t\t}\n\t}\n\tdefer pinger.Close()\n\n\tif pinger.PayloadSize() != uint16(size) {\n\t\tpinger.SetPayloadSize(uint16(size))\n\t}\n\n\tif remoteAddr.IP.IsLinkLocalMulticast() {\n\t\tmulticastPing()\n\t} else {\n\t\tunicastPing()\n\t}\n}\n\nfunc unicastPing() {\n\trtt, err := pinger.PingAttempts(remoteAddr, timeout, int(attempts))\n\n\tif err != nil {\n\t\tfmt.Println(err)\n\t\tos.Exit(1)\n\t}\n\n\tfmt.Printf(\"ping %s (%s) rtt=%v\\n\", destination, remoteAddr, rtt)\n}\n\nfunc multicastPing() {\n\tfmt.Printf(\"multicast ping to %s (%s)\\n\", args[0], destination)\n\n\tresponses, err := pinger.PingMulticast(remoteAddr, timeout)\n\n\tif err != nil {\n\t\tfmt.Println(err)\n\t\tos.Exit(1)\n\t}\n\n\tfor response := range responses {\n\t\tfmt.Printf(\"%+v\\n\", response)\n\t}\n}\n"
  },
  {
    "path": "cmd/pingnet/Makefile",
    "content": "TARGET = pingnet\n\ninclude ../common.mk\n\n.PHONY: test\ntest: all\n\t./$(TARGET) -c 1 -w 100ms -f 127.0.0.1/24\n"
  },
  {
    "path": "cmd/pingnet/main.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"os\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\tping \"github.com/digineo/go-ping\"\n\t\"gopkg.in/cheggaaa/pb.v1\"\n)\n\nvar (\n\ttimeout  = 5 * time.Second\n\tattempts = 3\n\tpoolSize = 2 * runtime.NumCPU()\n\tinterval = 100 * time.Millisecond\n\tifname   = \"\"\n\tbind6    = \"::\"\n\tbind4    = \"0.0.0.0\"\n\tsize     = uint(56)\n\tforce    bool\n\tverbose  bool\n\tmark     uint\n\tpinger   *ping.Pinger\n)\n\ntype workGenerator struct {\n\tip  net.IP\n\tnet *net.IPNet\n}\n\nfunc (w *workGenerator) size() uint64 {\n\tones, bits := w.net.Mask.Size()\n\treturn 1 << uint64(bits-ones)\n}\n\nfunc (w *workGenerator) each(callback func(net.IP) error) error {\n\t// adapted from http://play.golang.org/p/m8TNTtygK0\n\tinc := func(ip net.IP) net.IP {\n\t\tres := make(net.IP, len(ip))\n\t\tcopy(res, ip)\n\t\tfor j := len(res) - 1; j >= 0; j-- {\n\t\t\tres[j]++\n\t\t\tif res[j] > 0 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\treturn res\n\t}\n\tfor ip := w.ip.Mask(w.net.Mask); w.net.Contains(ip); ip = inc(ip) {\n\t\tif err := callback(ip); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\ntype result struct {\n\taddr net.IPAddr\n\trtt  time.Duration\n\terr  error\n}\n\nfunc main() {\n\tlog.SetFlags(0)\n\n\tflag.Usage = func() {\n\t\tfmt.Fprintln(os.Stderr, \"Usage:\", os.Args[0], \"[options] CIDR [CIDR [...]]\")\n\t\tflag.PrintDefaults()\n\t}\n\n\tflag.IntVar(&attempts, \"c\", attempts, \"number of ping attempts per address\")\n\tflag.DurationVar(&timeout, \"w\", timeout, \"timeout for a single echo request\")\n\tflag.DurationVar(&interval, \"i\", interval, \"CIDR iteration interval\")\n\tflag.UintVar(&size, \"s\", size, \"size of additional payload data\")\n\tflag.StringVar(&bind4, \"4\", bind4, \"IPv4 bind address\")\n\tflag.StringVar(&bind6, \"6\", bind6, \"IPv6 bind address\")\n\tflag.StringVar(&ifname, \"I\", ifname, \"interface name/IPv6 zone\")\n\tflag.IntVar(&poolSize, \"P\", poolSize, \"concurrency level\")\n\tflag.BoolVar(&force, \"f\", force, \"sanity flag needed if you want to ping more than 4096 hosts (/20)\")\n\tflag.BoolVar(&verbose, \"v\", verbose, \"also print out unreachable addresses\")\n\tflag.UintVar(&mark, \"m\", mark, \"set socket mark (SO_MARK) to this value\")\n\tflag.Parse()\n\n\t// simple error checking\n\tif bind4 == \"\" && bind6 == \"\" {\n\t\tlog.Fatal(\"need at least an IPv4 (-bind4 flag) or IPv6 (-bind6 flag) address to bind to\")\n\t}\n\tif poolSize <= 0 {\n\t\tlog.Fatal(\"concurrency level (-P flag) must be > 0\")\n\t}\n\tif attempts <= 0 {\n\t\tlog.Fatal(\"number of ping attempts (-c flag) must be > 0\")\n\t}\n\n\t// parse CIDR arguments\n\ttotal := uint64(0)\n\tgenerator := make([]*workGenerator, 0, flag.NArg())\n\tfor _, cidr := range flag.Args() {\n\t\tip, ipnet, err := net.ParseCIDR(cidr)\n\t\tif err != nil {\n\t\t\tlog.Println(err)\n\t\t\tcontinue\n\t\t}\n\t\tw := &workGenerator{ip: ip, net: ipnet}\n\t\tgenerator = append(generator, w)\n\t\ttotal += w.size()\n\t}\n\n\tif total == 0 {\n\t\t// no (valid) CIDR argument given\n\t\tflag.Usage()\n\t\tos.Exit(1)\n\t} else if total > 4096 && !force {\n\t\t// expanding all arguments yields too many addresses\n\t\tlog.Printf(\"You want to ping %d hosts. If that is correct, try again with -f flag\", total)\n\t\tos.Exit(1)\n\t}\n\n\tif p, err := ping.New(bind4, bind6); err != nil {\n\t\tlog.Fatal(err)\n\t} else {\n\t\tpinger = p\n\t}\n\n\tif mark > 0 {\n\t\tpinger.SetMark(mark)\n\t}\n\n\t// prepare worker\n\twg := &sync.WaitGroup{}\n\twg.Add(poolSize)\n\tips := make(chan net.IPAddr, poolSize)\n\tres := make(chan *result, poolSize)\n\n\tfor i := 0; i < poolSize; i++ {\n\t\tgo func() {\n\t\t\tfor ip := range ips {\n\t\t\t\tvar err error\n\t\t\t\tvar rtt time.Duration\n\t\t\t\tfor i := 1; ; i++ {\n\t\t\t\t\trtt, err = pinger.PingAttempts(&ip, timeout, attempts)\n\t\t\t\t\tif err == nil || !strings.Contains(err.Error(), \"no buffer space available\") {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\ttime.Sleep(timeout * time.Duration(i))\n\t\t\t\t}\n\n\t\t\t\tres <- &result{addr: ip, rtt: rtt, err: err}\n\t\t\t}\n\t\t\twg.Done()\n\t\t}()\n\t}\n\n\t// printer\n\tpr := &sync.WaitGroup{}\n\tpr.Add(1)\n\tgo func() {\n\t\tbar := pb.New64(int64(total))\n\t\tbar.ShowBar = true\n\t\tbar.ShowTimeLeft = true\n\t\tbar.ShowCounters = true\n\t\tbar.Start()\n\t\tconst clear = \"\\x1b[2K\\r\" // ansi delete line + CR\n\n\t\tfor r := range res {\n\t\t\tbar.Increment()\n\t\t\tif r.err == nil {\n\t\t\t\tlog.Printf(\"%s%s - rtt=%v\", clear, r.addr.IP, r.rtt)\n\t\t\t\tbar.Update()\n\t\t\t} else if verbose {\n\t\t\t\tlog.Printf(\"%s%s - %v\", clear, r.addr, r.err)\n\t\t\t\tbar.Update()\n\t\t\t}\n\t\t}\n\n\t\tbar.Finish()\n\t\tpr.Done()\n\t}()\n\n\t// yield all IP addresses\n\tfor _, g := range generator {\n\t\tg.each(func(ip net.IP) error {\n\t\t\tips <- net.IPAddr{IP: ip, Zone: ifname}\n\t\t\ttime.Sleep(interval)\n\t\t\treturn nil\n\t\t})\n\t}\n\n\t// wait for worker and printer to finish\n\tclose(ips)\n\twg.Wait()\n\tclose(res)\n\tpr.Wait()\n}\n"
  },
  {
    "path": "error.go",
    "content": "package ping\n\nimport \"errors\"\n\nvar (\n\terrClosed   = errors.New(\"pinger closed\")\n\terrNotBound = errors.New(\"need at least one bind address\")\n)\n\n// timeoutError implements the net.Error interface. Originally taken from\n// https://github.com/golang/go/blob/release-branch.go1.8/src/net/net.go#L505-L509\ntype timeoutError struct{}\n\nfunc (e *timeoutError) Error() string   { return \"i/o timeout\" }\nfunc (e *timeoutError) Timeout() bool   { return true }\nfunc (e *timeoutError) Temporary() bool { return true }\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/digineo/go-ping\n\ngo 1.23.0\n\nrequire (\n\tgithub.com/digineo/go-logwrap v0.0.0-20181106161722-a178c58ea3f0\n\tgithub.com/gdamore/tcell/v2 v2.9.0\n\tgithub.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb\n\tgithub.com/stretchr/testify v1.11.0\n\tgolang.org/x/net v0.43.0\n\tgopkg.in/cheggaaa/pb.v1 v1.0.28\n)\n\nrequire (\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/fatih/color v1.7.0 // indirect\n\tgithub.com/gdamore/encoding v1.0.1 // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.2.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.0 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.16 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgolang.org/x/sys v0.35.0 // indirect\n\tgolang.org/x/term v0.34.0 // indirect\n\tgolang.org/x/text v0.28.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/digineo/go-logwrap v0.0.0-20181106161722-a178c58ea3f0 h1:OT/LKmj81wMymnWXaKaKBR9n1vPlu+GC0VVKaZP6kzs=\ngithub.com/digineo/go-logwrap v0.0.0-20181106161722-a178c58ea3f0/go.mod h1:DmqdumeAKGQNU5E8MN0ruT5ZGx8l/WbAsMbXCXcSEts=\ngithub.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=\ngithub.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=\ngithub.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=\ngithub.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=\ngithub.com/gdamore/tcell/v2 v2.9.0 h1:N6t+eqK7/xwtRPwxzs1PXeRWnm0H9l02CrgJ7DLn1ys=\ngithub.com/gdamore/tcell/v2 v2.9.0/go.mod h1:8/ZoqM9rxzYphT9tH/9LnunhV9oPBqwS8WHGYm5nrmo=\ngithub.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=\ngithub.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/mattn/go-colorable v0.1.0 h1:v2XXALHHh6zHfYTJ+cSkwtyffnaOyR1MXaA91mTrb8o=\ngithub.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=\ngithub.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb h1:n7UJ8X9UnrTZBYXnd1kAIBc067SWyuPIrsocjketYW8=\ngithub.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=\ngithub.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=\ngolang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=\ngolang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=\ngolang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=\ngolang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk=\ngopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "monitor/history.go",
    "content": "package monitor\n\nimport (\n\t\"math\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n)\n\n// Result stores the information about a single ping, in particular\n// the round-trip time or whether the packet was lost.\ntype Result struct {\n\tRTT  time.Duration\n\tLost bool\n}\n\n// History represents the ping history for a single node/device.\ntype History struct {\n\tresults  []Result\n\tcount    int\n\tposition int\n\tsync.RWMutex\n}\n\n// NewHistory creates a new History object with a specific capacity\nfunc NewHistory(capacity int) History {\n\treturn History{\n\t\tresults: make([]Result, capacity),\n\t}\n}\n\n// AddResult saves a ping result into the internal history.\nfunc (h *History) AddResult(rtt time.Duration, err error) {\n\th.Lock()\n\n\th.results[h.position] = Result{RTT: rtt, Lost: err != nil}\n\th.position = (h.position + 1) % cap(h.results)\n\n\tif h.count < cap(h.results) {\n\t\th.count++\n\t}\n\n\th.Unlock()\n}\n\nfunc (h *History) clear() {\n\th.count = 0\n\th.position = 0\n}\n\n// ComputeAndClear aggregates the result history into a single data point and clears the result set.\nfunc (h *History) ComputeAndClear() *Metrics {\n\th.Lock()\n\tresult := h.compute()\n\th.clear()\n\th.Unlock()\n\treturn result\n}\n\n// Compute aggregates the result history into a single data point.\nfunc (h *History) Compute() *Metrics {\n\th.RLock()\n\tdefer h.RUnlock()\n\treturn h.compute()\n}\n\nfunc (h *History) compute() *Metrics {\n\tnumFailure := 0\n\tnumTotal := h.count\n\tµsPerMs := 1.0 / float64(time.Millisecond)\n\n\tif numTotal == 0 {\n\t\treturn nil\n\t}\n\n\tdata := make([]float64, 0, numTotal)\n\tvar best, worst, mean, stddev, total, sumSquares float64\n\tvar extremeFound bool\n\n\tfor i := 0; i < numTotal; i++ {\n\t\tcurr := &h.results[i]\n\t\tif curr.Lost {\n\t\t\tnumFailure++\n\t\t} else {\n\t\t\trtt := float64(curr.RTT) * µsPerMs\n\t\t\tdata = append(data, rtt)\n\n\t\t\tif !extremeFound || rtt < best {\n\t\t\t\tbest = rtt\n\t\t\t}\n\t\t\tif !extremeFound || rtt > worst {\n\t\t\t\tworst = rtt\n\t\t\t}\n\n\t\t\textremeFound = true\n\t\t\ttotal += rtt\n\t\t}\n\t}\n\n\tsize := float64(numTotal - numFailure)\n\tmean = total / size\n\tfor _, rtt := range data {\n\t\tdiff := rtt - mean\n\t\tsumSquares += diff * diff\n\t}\n\tstddev = math.Sqrt(sumSquares / size)\n\n\tmedian := math.NaN()\n\tif l := len(data); l > 0 {\n\t\tsort.Float64Slice(data).Sort()\n\t\tif l%2 == 0 {\n\t\t\tmedian = (data[l/2-1] + data[l/2]) / 2\n\t\t} else {\n\t\t\tmedian = data[l/2]\n\t\t}\n\t}\n\n\treturn &Metrics{\n\t\tPacketsSent: numTotal,\n\t\tPacketsLost: numFailure,\n\t\tBest:        float32(best),\n\t\tWorst:       float32(worst),\n\t\tMedian:      float32(median),\n\t\tMean:        float32(mean),\n\t\tStdDev:      float32(stddev),\n\t}\n}\n"
  },
  {
    "path": "monitor/history_test.go",
    "content": "package monitor\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc BenchmarkAddResult(b *testing.B) {\n\th := NewHistory(8)\n\tfor i := 0; i < b.N; i++ {\n\t\th.AddResult(time.Duration(i), nil) // 1 allocc\n\t}\n}\n\nfunc BenchmarkCompute(b *testing.B) {\n\th := NewHistory(8)\n\tfor i := 0; i < b.N; i++ {\n\t\th.AddResult(time.Duration(i), nil) // 1 alloc\n\t\th.Compute()                        // 2 allocs\n\t}\n}\n\nfunc TestCompute(t *testing.T) {\n\tassert := assert.New(t)\n\tconst dur = 100 * time.Millisecond\n\terr := fmt.Errorf(\"i/o timeout\")\n\n\t{ // empty list\n\t\th := NewHistory(4)\n\t\tassert.Nil(h.Compute())\n\t}\n\n\t{ // one failed entry\n\t\th := NewHistory(4)\n\t\th.AddResult(2, err)\n\n\t\tmetrics := h.Compute()\n\t\tassert.EqualValues(1, metrics.PacketsSent)\n\t\tassert.EqualValues(1, metrics.PacketsLost)\n\t\tassert.EqualValues(0, metrics.Best)\n\t\tassert.EqualValues(0, metrics.Worst)\n\t\tassert.True(math.IsNaN(float64(metrics.Median)))\n\t\tassert.True(math.IsNaN(float64(metrics.Mean)))\n\t\tassert.True(math.IsNaN(float64(metrics.StdDev)))\n\t}\n\n\t{ // populate with 5 entries\n\t\th := NewHistory(8)\n\t\th.AddResult(0, nil)\n\t\th.AddResult(dur, nil)\n\t\th.AddResult(dur, nil)\n\t\th.AddResult(0, err)\n\t\th.AddResult(dur, nil)\n\n\t\tassert.Equal(h.count, 5)\n\t\tassert.EqualValues(1, h.Compute().PacketsLost)\n\t}\n\n\t{ // test median\n\t\th := NewHistory(5)\n\t\th.AddResult(3*dur, nil)\n\t\th.AddResult(2*dur, nil)\n\t\th.AddResult(1*dur, nil)\n\t\th.AddResult(0*dur, nil)\n\t\tassert.EqualValues(150, h.Compute().Median)\n\n\t\th.AddResult(4*dur, nil)\n\t\tassert.EqualValues(200, h.Compute().Median)\n\t}\n\n\t{\n\t\t// test zero variance\n\t\th := NewHistory(8)\n\t\th.AddResult(dur, nil)\n\t\th.AddResult(dur, nil)\n\t\th.AddResult(0, err)\n\n\t\tmetrics := h.Compute()\n\t\tassert.EqualValues(100, metrics.Best)\n\t\tassert.EqualValues(100, metrics.Worst)\n\t\tassert.EqualValues(100, metrics.Mean)\n\t\tassert.EqualValues(100, metrics.Median)\n\t\tassert.EqualValues(0, metrics.StdDev)\n\t\tassert.EqualValues(3, metrics.PacketsSent)\n\t\tassert.EqualValues(1, metrics.PacketsLost)\n\n\t\t// results getting worse\n\t\th.AddResult(2*dur, nil)\n\t\th.AddResult(dur, nil)\n\t\th.AddResult(0, err)\n\n\t\tmetrics = h.Compute()\n\t\tassert.EqualValues(100, metrics.Best)\n\t\tassert.EqualValues(200, metrics.Worst)\n\t\tassert.EqualValues(125, metrics.Mean)\n\t\tassert.EqualValues(100, metrics.Median)\n\t\tassert.InDelta(43.30127, float64(metrics.StdDev), 0.000001)\n\t\tassert.EqualValues(6, metrics.PacketsSent)\n\t\tassert.EqualValues(2, metrics.PacketsLost)\n\n\t\t// finally something better\n\t\th.AddResult(0, nil)\n\t\tmetrics = h.Compute()\n\t\tassert.EqualValues(0, metrics.Best)\n\t\tassert.EqualValues(200, metrics.Worst)\n\t\tassert.EqualValues(100, metrics.Mean)\n\t\tassert.EqualValues(100, metrics.Median)\n\t\tassert.InDelta(63.2455, float64(metrics.StdDev), 0.0001)\n\t\tassert.EqualValues(7, metrics.PacketsSent)\n\t\tassert.EqualValues(2, metrics.PacketsLost)\n\t}\n}\n\nfunc TestHistoryCapacity(t *testing.T) {\n\tassert := assert.New(t)\n\terr := fmt.Errorf(\"i/o timeout\")\n\n\th := NewHistory(3)\n\tassert.Equal(h.count, 0)\n\th.AddResult(1, nil)\n\th.AddResult(2, err)\n\tassert.Equal(h.count, 2)\n\tassert.Equal(h.position, 2)\n\th.AddResult(1, nil)\n\tassert.Equal(h.count, 3)\n\tassert.Equal(h.position, 0)\n\n\th.AddResult(0, nil)\n\tassert.Equal(h.count, 3)\n\tassert.Equal(h.position, 1)\n\tassert.EqualValues(1, h.Compute().PacketsLost)\n\n\t// overwrite lost packet result\n\th.AddResult(0, nil)\n\tassert.EqualValues(0, h.Compute().PacketsLost)\n\n\t// clear\n\th.ComputeAndClear()\n\tassert.Equal(h.count, 0)\n\tassert.Equal(h.position, 0)\n}\n"
  },
  {
    "path": "monitor/metrics.go",
    "content": "package monitor\n\n// Metrics is a dumb data point computed from a history of Results.\ntype Metrics struct {\n\tPacketsSent int     // number of packets sent\n\tPacketsLost int     // number of packets lost\n\tBest        float32 // best rtt in ms\n\tWorst       float32 // worst rtt in ms\n\tMedian      float32 // median rtt in ms\n\tMean        float32 // mean rtt in ms\n\tStdDev      float32 // std deviation in ms\n}\n"
  },
  {
    "path": "monitor/monitor.go",
    "content": "package monitor\n\nimport (\n\t\"net\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/digineo/go-ping\"\n)\n\n// Monitor manages the goroutines responsible for collecting Ping RTT data.\ntype Monitor struct {\n\tHistorySize int // Number of results per target to keep\n\n\tpinger   *ping.Pinger\n\tinterval time.Duration\n\ttargets  map[string]*Target\n\tmtx      sync.RWMutex\n\ttimeout  time.Duration\n}\n\nconst defaultHistorySize = 10\n\n// New creates and configures a new Ping instance. You need to call\n// AddTarget()/RemoveTarget() to manage monitored targets.\nfunc New(pinger *ping.Pinger, interval, timeout time.Duration) *Monitor {\n\treturn &Monitor{\n\t\tpinger:      pinger,\n\t\tinterval:    interval,\n\t\ttimeout:     timeout,\n\t\ttargets:     make(map[string]*Target),\n\t\tHistorySize: defaultHistorySize,\n\t}\n}\n\n// Stop brings the monitoring gracefully to a halt.\nfunc (p *Monitor) Stop() {\n\tp.mtx.Lock()\n\tfor id := range p.targets {\n\t\tp.removeTarget(id)\n\t}\n\tp.pinger.Close()\n\tp.mtx.Unlock()\n}\n\n// AddTarget adds a target to the monitored list. If the target with the given\n// ID already exists, it is removed first and then readded. This allows\n// the easy restart of the monitoring.\nfunc (p *Monitor) AddTarget(key string, addr net.IPAddr) (err error) {\n\treturn p.AddTargetDelayed(key, addr, 0)\n}\n\n// AddTargetDelayed is AddTarget with a startup delay\nfunc (p *Monitor) AddTargetDelayed(key string, addr net.IPAddr, startupDelay time.Duration) (err error) {\n\tp.mtx.Lock()\n\tdefer p.mtx.Unlock()\n\n\ttarget, err := newTarget(p.interval, p.timeout, startupDelay, p.HistorySize, p.pinger, addr)\n\tif err != nil {\n\t\treturn err\n\t}\n\tp.removeTarget(key)\n\tp.targets[key] = target\n\treturn\n}\n\n// RemoveTarget removes a target from the monitoring list.\nfunc (p *Monitor) RemoveTarget(key string) {\n\tp.mtx.Lock()\n\tp.removeTarget(key)\n\tp.mtx.Unlock()\n}\n\n// Stops monitoring a target and removes it from the list (if the list includes\n// the target). Needs to be locked externally!\nfunc (p *Monitor) removeTarget(key string) {\n\ttarget, found := p.targets[key]\n\tif !found {\n\t\treturn\n\t}\n\ttarget.Stop()\n\tdelete(p.targets, key)\n}\n\n// ExportAndClear calculates the metrics for each monitored target, cleans the result set and\n// returns it as a simple map.\nfunc (p *Monitor) ExportAndClear() map[string]*Metrics {\n\treturn p.export(true)\n}\n\n// Export calculates the metrics for each monitored target and returns it as a simple map.\nfunc (p *Monitor) Export() map[string]*Metrics {\n\treturn p.export(false)\n}\n\nfunc (p *Monitor) export(clear bool) map[string]*Metrics {\n\tm := make(map[string]*Metrics)\n\n\tp.mtx.RLock()\n\tdefer p.mtx.RUnlock()\n\n\tfor id, target := range p.targets {\n\t\tif metrics := target.Compute(clear); metrics != nil {\n\t\t\tm[id] = metrics\n\t\t}\n\t}\n\treturn m\n}\n"
  },
  {
    "path": "monitor/target.go",
    "content": "package monitor\n\nimport (\n\t\"net\"\n\t\"sync\"\n\t\"time\"\n\n\tping \"github.com/digineo/go-ping\"\n)\n\n// Target is a unit of work\ntype Target struct {\n\tpinger   *ping.Pinger\n\taddr     net.IPAddr\n\tinterval time.Duration\n\ttimeout  time.Duration\n\tstop     chan struct{}\n\thistory  History\n\twg       sync.WaitGroup\n}\n\n// newTarget starts a new monitoring goroutine\nfunc newTarget(interval, timeout, startupDelay time.Duration, historySize int, pinger *ping.Pinger, addr net.IPAddr) (*Target, error) {\n\tn := &Target{\n\t\tpinger:   pinger,\n\t\taddr:     addr,\n\t\tinterval: interval,\n\t\ttimeout:  timeout,\n\t\tstop:     make(chan struct{}),\n\t\thistory:  NewHistory(historySize),\n\t}\n\tn.wg.Add(1)\n\tgo n.run(startupDelay)\n\treturn n, nil\n}\n\nfunc (n *Target) run(startupDelay time.Duration) {\n\tif startupDelay > 0 {\n\t\tselect {\n\t\tcase <-time.After(startupDelay):\n\t\tcase <-n.stop:\n\t\t}\n\t}\n\n\ttick := time.NewTicker(n.interval)\n\tfor {\n\t\tselect {\n\t\tcase <-n.stop:\n\t\t\ttick.Stop()\n\t\t\tn.wg.Done()\n\t\t\treturn\n\t\tcase <-tick.C:\n\t\t\tgo n.ping()\n\t\t}\n\t}\n}\n\n// Stop gracefully stops the monitoring.\nfunc (n *Target) Stop() {\n\tclose(n.stop)\n\tn.wg.Wait()\n}\n\n// Compute returns the computed ping metrics for this node and optonally clears the result set.\nfunc (n *Target) Compute(clear bool) *Metrics {\n\tif clear {\n\t\treturn n.history.ComputeAndClear()\n\t}\n\treturn n.history.Compute()\n}\n\nfunc (n *Target) ping() {\n\tn.history.AddResult(n.pinger.Ping(&n.addr, n.timeout))\n}\n"
  },
  {
    "path": "payload.go",
    "content": "package ping\n\nimport (\n\t\"math/rand\"\n\t\"time\"\n\n\t\"github.com/digineo/go-logwrap\"\n)\n\nvar (\n\tlog = &logwrap.Instance{}\n\n\t// SetLogger allows updating the Logger. For details, see\n\t// \"github.com/digineo/go-logwrap\".Instance.SetLogger.\n\tSetLogger = log.SetLogger\n\n\t// SA1019: rand.Seed has been deprecated, provide package-local RNG\n\trng = rand.New(rand.NewSource(time.Now().UnixNano()))\n)\n\n// Payload represents additional data appended to outgoing ICMP Echo\n// Requests.\ntype Payload []byte\n\n// Resize will assign a new payload of the given size to p.\nfunc (p *Payload) Resize(size uint16) {\n\tbuf := make([]byte, size)\n\tif _, err := rng.Read(buf); err != nil {\n\t\tlog.Errorf(\"error resizing payload: %v\", err)\n\t\treturn\n\t}\n\t*p = Payload(buf)\n}\n"
  },
  {
    "path": "pinger.go",
    "content": "package ping\n\nimport (\n\t\"net\"\n\t\"os\"\n\t\"sync\"\n\n\t\"golang.org/x/net/icmp\"\n)\n\nconst (\n\t// ProtocolICMP is the number of the Internet Control Message Protocol\n\t// (see golang.org/x/net/internal/iana.ProtocolICMP)\n\tProtocolICMP = 1\n\n\t// ProtocolICMPv6 is the IPv6 Next Header value for ICMPv6\n\t// see golang.org/x/net/internal/iana.ProtocolIPv6ICMP\n\tProtocolICMPv6 = 58\n)\n\n// default sequence counter for this process\nvar sequence uint32\n\n// Pinger is a instance for ICMP echo requests\ntype Pinger struct {\n\tLogUnexpectedPackets bool // increases log verbosity\n\tId                   uint16\n\tSequenceCounter      *uint32\n\n\tpayload   Payload\n\tpayloadMu sync.RWMutex\n\n\trequests map[uint32]request // currently running requests\n\tmtx      sync.RWMutex       // lock for the requests map\n\tconn4    net.PacketConn\n\tconn6    net.PacketConn\n\twrite4   sync.Mutex // lock for conn4.WriteTo\n\twrite6   sync.Mutex // lock for conn6.WriteTo\n\twg       sync.WaitGroup\n}\n\n// New creates a new Pinger. This will open the raw socket and start the\n// receiving logic. You'll need to call Close() to cleanup.\nfunc New(bind4, bind6 string) (*Pinger, error) {\n\t// open sockets\n\tconn4, err := connectICMP(\"ip4:icmp\", bind4)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconn6, err := connectICMP(\"ip6:ipv6-icmp\", bind6)\n\tif err != nil {\n\t\tif conn4 != nil {\n\t\t\tconn4.Close()\n\t\t}\n\t\treturn nil, err\n\t}\n\n\tif conn4 == nil && conn6 == nil {\n\t\treturn nil, errNotBound\n\t}\n\n\tpinger := Pinger{\n\t\tconn4:           conn4,\n\t\tconn6:           conn6,\n\t\tId:              uint16(os.Getpid()),\n\t\tSequenceCounter: &sequence,\n\t\trequests:        make(map[uint32]request),\n\t}\n\tpinger.SetPayloadSize(56)\n\n\tif conn4 != nil {\n\t\tpinger.wg.Add(1)\n\t\tgo pinger.receiver(ProtocolICMP, pinger.conn4)\n\t}\n\tif conn6 != nil {\n\t\tpinger.wg.Add(1)\n\t\tgo pinger.receiver(ProtocolICMPv6, pinger.conn6)\n\t}\n\n\treturn &pinger, nil\n}\n\n// Close will close the ICMP socket.\nfunc (pinger *Pinger) Close() {\n\tpinger.close(pinger.conn4)\n\tpinger.close(pinger.conn6)\n\tpinger.wg.Wait()\n}\n\n// connectICMP opens a new ICMP connection, if network and address are not empty.\nfunc connectICMP(network, address string) (*icmp.PacketConn, error) {\n\tif network == \"\" || address == \"\" {\n\t\treturn nil, nil\n\t}\n\n\treturn icmp.ListenPacket(network, address)\n}\n\nfunc (pinger *Pinger) close(conn net.PacketConn) {\n\tif conn != nil {\n\t\tconn.Close()\n\t}\n}\n\nfunc (pinger *Pinger) removeRequest(idseq uint32) {\n\tpinger.mtx.Lock()\n\tdelete(pinger.requests, idseq)\n\tpinger.mtx.Unlock()\n}\n\n// SetPayloadSize resizes additional payload data to the given size. The\n// payload will subsequently be appended to outgoing ICMP Echo Requests.\n//\n// The default payload size is 56, resulting in 64 bytes for the ICMP packet.\nfunc (pinger *Pinger) SetPayloadSize(size uint16) {\n\tpinger.payloadMu.Lock()\n\tpinger.payload.Resize(size)\n\tpinger.payloadMu.Unlock()\n}\n\n// SetPayload allows you to overwrite the current payload with your own data.\nfunc (pinger *Pinger) SetPayload(data []byte) {\n\tpinger.payloadMu.Lock()\n\tpinger.payload = Payload(data)\n\tpinger.payloadMu.Unlock()\n}\n\n// PayloadSize retrieves the current payload size.\nfunc (pinger *Pinger) PayloadSize() uint16 {\n\tpinger.payloadMu.RLock()\n\tdefer pinger.payloadMu.RUnlock()\n\treturn uint16(len(pinger.payload))\n}\n"
  },
  {
    "path": "pinger_linux.go",
    "content": "package ping\r\n\r\nimport (\r\n\t\"errors\"\r\n\t\"os\"\r\n\t\"reflect\"\r\n\t\"syscall\"\r\n\r\n\t\"golang.org/x/net/icmp\"\r\n)\r\n\r\n// getFD gets the system file descriptor for an icmp.PacketConn\r\nfunc getFD(c *icmp.PacketConn) (uintptr, error) {\r\n\tv := reflect.ValueOf(c).Elem().FieldByName(\"c\").Elem()\r\n\tif v.Elem().Kind() != reflect.Struct {\r\n\t\treturn 0, errors.New(\"invalid type\")\r\n\t}\r\n\r\n\tfd := v.Elem().FieldByName(\"conn\").FieldByName(\"fd\")\r\n\tif fd.Elem().Kind() != reflect.Struct {\r\n\t\treturn 0, errors.New(\"invalid type\")\r\n\t}\r\n\r\n\tpfd := fd.Elem().FieldByName(\"pfd\")\r\n\tif pfd.Kind() != reflect.Struct {\r\n\t\treturn 0, errors.New(\"invalid type\")\r\n\t}\r\n\r\n\treturn uintptr(pfd.FieldByName(\"Sysfd\").Int()), nil\r\n}\r\n\r\nfunc (pinger *Pinger) SetMark(mark uint) error {\r\n\tconn4, ok := pinger.conn4.(*icmp.PacketConn)\r\n\tif !ok {\r\n\t\treturn errors.New(\"invalid connection type\")\r\n\t}\r\n\r\n\tfd, err := getFD(conn4)\r\n\tif err != nil {\r\n\t\treturn err\r\n\t}\r\n\r\n\terr = os.NewSyscallError(\r\n\t\t\"setsockopt\",\r\n\t\tsyscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(mark)),\r\n\t)\r\n\r\n\tif err != nil {\r\n\t\treturn err\r\n\t}\r\n\r\n\tconn6, ok := pinger.conn6.(*icmp.PacketConn)\r\n\tif !ok {\r\n\t\treturn errors.New(\"invalid connection type\")\r\n\t}\r\n\r\n\tfd, err = getFD(conn6)\r\n\tif err != nil {\r\n\t\treturn err\r\n\t}\r\n\r\n\treturn os.NewSyscallError(\r\n\t\t\"setsockopt\",\r\n\t\tsyscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(mark)),\r\n\t)\r\n}\r\n"
  },
  {
    "path": "pinger_other.go",
    "content": "//go:build !linux\r\n\r\npackage ping\r\n\r\nimport \"errors\"\r\n\r\nfunc (pinger *Pinger) SetMark(mark uint) error {\r\n\treturn errors.New(\"setting SO_MARK socket option is not supported on this platform\")\r\n}\r\n"
  },
  {
    "path": "pinger_test.go",
    "content": "package ping\n\nimport (\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPinger(t *testing.T) {\n\tassert := assert.New(t)\n\trequire := require.New(t)\n\n\tpinger, err := New(\"0.0.0.0\", \"::\")\n\trequire.NoError(err)\n\trequire.NotNil(pinger)\n\tdefer pinger.Close()\n\n\tfor _, target := range []string{\"127.0.0.1\", \"::1\"} {\n\t\trtt, err := pinger.PingAttempts(&net.IPAddr{IP: net.ParseIP(target)}, time.Second, 2)\n\t\tassert.NoError(err, target)\n\t\tassert.NotZero(rtt, target)\n\t}\n}\n"
  },
  {
    "path": "receiving.go",
    "content": "package ping\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"time\"\n\n\t\"golang.org/x/net/icmp\"\n\t\"golang.org/x/net/ipv4\"\n\t\"golang.org/x/net/ipv6\"\n)\n\n// receiver listens on the raw socket and correlates ICMP Echo Replys with\n// currently running requests.\nfunc (pinger *Pinger) receiver(proto int, conn net.PacketConn) {\n\trb := make([]byte, 1500)\n\n\t// read incoming packets\n\tfor {\n\t\tif n, source, err := conn.ReadFrom(rb); err != nil {\n\t\t\tif netErr, ok := err.(net.Error); !ok || !netErr.Temporary() { //nolint:staticcheck\n\t\t\t\tbreak // socket gone\n\t\t\t}\n\t\t} else {\n\t\t\tpinger.receive(proto, rb[:n], source.(*net.IPAddr).IP, time.Now())\n\t\t}\n\t}\n\n\t// close running requests\n\tpinger.mtx.RLock()\n\tfor _, req := range pinger.requests {\n\t\treq.handleReply(errClosed, nil, nil)\n\t}\n\tpinger.mtx.RUnlock()\n\n\t// Close() waits for us\n\tpinger.wg.Done()\n}\n\n// receive takes the raw message and tries to evaluate an ICMP response.\n// If that succeeds, the body will given to process() for further processing.\nfunc (pinger *Pinger) receive(proto int, bytes []byte, addr net.IP, t time.Time) {\n\t// parse message\n\tm, err := icmp.ParseMessage(proto, bytes)\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// evaluate message\n\tswitch m.Type {\n\tcase ipv4.ICMPTypeEchoReply, ipv6.ICMPTypeEchoReply:\n\t\tpinger.process(m.Body, nil, addr, &t)\n\n\tcase ipv4.ICMPTypeDestinationUnreachable, ipv6.ICMPTypeDestinationUnreachable:\n\t\tbody := m.Body.(*icmp.DstUnreach)\n\t\tif body == nil {\n\t\t\treturn\n\t\t}\n\n\t\tvar bodyData []byte\n\t\tswitch proto {\n\t\tcase ProtocolICMP:\n\t\t\t// parse header of original IPv4 packet\n\t\t\thdr, err := ipv4.ParseHeader(body.Data)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tbodyData = body.Data[hdr.Len:]\n\t\tcase ProtocolICMPv6:\n\t\t\t// parse header of original IPv6 packet (we don't need the actual\n\t\t\t// header, but want to detect parsing errors)\n\t\t\t_, err := ipv6.ParseHeader(body.Data)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tbodyData = body.Data[ipv6.HeaderLen:]\n\t\tdefault:\n\t\t\treturn\n\t\t}\n\n\t\t// parse ICMP message after the IP header\n\t\tmsg, err := icmp.ParseMessage(proto, bodyData)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tpinger.process(msg.Body, fmt.Errorf(\"%s\", m.Type), nil, nil)\n\t}\n}\n\n// process will finish a currently running Echo Request, if the body is\n// an ICMP Echo reply to a request from us.\nfunc (pinger *Pinger) process(body icmp.MessageBody, result error, addr net.IP, tRecv *time.Time) {\n\techo, ok := body.(*icmp.Echo)\n\tif !ok || echo == nil {\n\t\tif pinger.LogUnexpectedPackets {\n\t\t\tlog.Infof(\"expected *icmp.Echo, got %#v\", body)\n\t\t}\n\t\treturn\n\t}\n\n\tidseq := (uint32(uint16(echo.ID)) << 16) | uint32(uint16(echo.Seq))\n\n\t// search for existing running echo request\n\tpinger.mtx.Lock()\n\treq := pinger.requests[idseq]\n\tif _, ok := req.(*simpleRequest); ok {\n\t\t// a simpleRequest is finished on the first reply\n\t\tdelete(pinger.requests, idseq)\n\t}\n\tpinger.mtx.Unlock()\n\n\tif req != nil {\n\t\treq.handleReply(result, addr, tRecv)\n\t}\n}\n"
  },
  {
    "path": "request.go",
    "content": "package ping\n\nimport (\n\t\"net\"\n\t\"sync\"\n\t\"time\"\n)\n\ntype request interface {\n\tinit()\n\tclose()\n\thandleReply(error, net.IP, *time.Time)\n}\n\n// A multiRequest is a currently running ICMP echo request waiting for multple answers.\ntype multiRequest struct {\n\ttStart  time.Time // when was the request packet sent?\n\treplies chan Reply\n\tclosed  bool\n\tmtx     sync.RWMutex\n}\n\n// Reply is a reply to a multicast echo request\ntype Reply struct {\n\tAddress  net.IP\n\tDuration time.Duration\n}\n\n// A simpleRequest is a currently running ICMP echo request waiting for a single answer.\ntype simpleRequest struct {\n\twait    chan struct{}\n\tresult  error\n\ttStart  time.Time  // when was this packet sent?\n\ttFinish *time.Time // if and when was the reply received?\n}\n\n// handleReply is responsible for finishing this request.\n// It takes an error as failure reason.\nfunc (req *simpleRequest) handleReply(err error, _ net.IP, tRecv *time.Time) {\n\treq.result = err\n\n\t// update tFinish only if no error present and value wasn't previously set\n\tif err == nil && tRecv != nil && req.tFinish == nil {\n\t\treq.tFinish = tRecv\n\t}\n\treq.close()\n}\n\nfunc (req *simpleRequest) init() {\n\treq.wait = make(chan struct{})\n\treq.tStart = time.Now()\n}\n\nfunc (req *simpleRequest) close() {\n\tdefer func() {\n\t\t// Double-closing is very unlikely, but a race condition may\n\t\t// happen when sending fails and a reply is received anyway.\n\t\trecover()\n\t}()\n\n\tclose(req.wait)\n}\n\nfunc (req *simpleRequest) roundTripTime() (time.Duration, error) {\n\tif req.result != nil {\n\t\treturn 0, req.result\n\t}\n\tif req.tFinish == nil {\n\t\treturn 0, nil\n\t}\n\treturn req.tFinish.Sub(req.tStart), nil\n}\n\nfunc (req *multiRequest) init() {\n\treq.replies = make(chan Reply)\n\treq.tStart = time.Now()\n}\n\nfunc (req *multiRequest) close() {\n\treq.mtx.Lock()\n\treq.closed = true\n\tclose(req.replies)\n\treq.mtx.Unlock()\n}\n\n// handleReply is responsible for adding a result to the result set\nfunc (req *multiRequest) handleReply(err error, addr net.IP, tRecv *time.Time) {\n\tif err != nil {\n\t\treturn\n\t}\n\t// avoid blocking\n\tgo func() {\n\t\treq.mtx.RLock()\n\t\tdefer req.mtx.RUnlock()\n\n\t\tif !req.closed {\n\t\t\treq.replies <- Reply{\n\t\t\t\tAddress:  addr,\n\t\t\t\tDuration: tRecv.Sub(req.tStart),\n\t\t\t}\n\t\t}\n\t}()\n}\n"
  },
  {
    "path": "sending.go",
    "content": "package ping\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"golang.org/x/net/icmp\"\n\t\"golang.org/x/net/ipv4\"\n\t\"golang.org/x/net/ipv6\"\n)\n\n// PingAttempts sends ICMP echo requests with a timeout per request, retrying upto `attempt` times .\n// Will finish early on success and return the round trip time of the last ping.\nfunc (pinger *Pinger) PingAttempts(destination *net.IPAddr, timeout time.Duration, attempts int) (rtt time.Duration, err error) {\n\tif attempts < 1 {\n\t\terr = errors.New(\"zero attempts\")\n\t} else {\n\t\tfor i := 0; i < attempts; i++ {\n\t\t\trtt, err = pinger.Ping(destination, timeout)\n\t\t\tif err == nil {\n\t\t\t\tbreak // success\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\n// Ping sends a single Echo Request and waits for an answer. It returns\n// the round trip time (RTT) if a reply is received in time.\nfunc (pinger *Pinger) Ping(destination *net.IPAddr, timeout time.Duration) (time.Duration, error) {\n\tctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(timeout))\n\tdefer cancel()\n\treturn pinger.PingContext(ctx, destination)\n}\n\n// PingContext sends a single Echo Request and waits for an answer. It returns\n// the round trip time (RTT) if a reply is received before cancellation of the context.\nfunc (pinger *Pinger) PingContext(ctx context.Context, destination *net.IPAddr) (time.Duration, error) {\n\treq := simpleRequest{}\n\n\tidseq, err := pinger.sendRequest(destination, &req)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\t// wait for answer\n\tselect {\n\tcase <-req.wait:\n\t\t// already dequeued\n\t\terr = req.result\n\tcase <-ctx.Done():\n\t\t// dequeue request\n\t\tpinger.removeRequest(idseq)\n\t\terr = &timeoutError{}\n\t}\n\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn req.roundTripTime()\n}\n\n// PingMulticast sends a single echo request and returns a channel for the responses.\n// The channel will be closed on termination of the context.\n// An error is returned if the sending of the echo request fails.\nfunc (pinger *Pinger) PingMulticast(destination *net.IPAddr, wait time.Duration) (<-chan Reply, error) {\n\tctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(wait))\n\tdefer cancel()\n\treturn pinger.PingMulticastContext(ctx, destination)\n}\n\n// PingMulticastContext does the same as PingMulticast but receives a context\nfunc (pinger *Pinger) PingMulticastContext(ctx context.Context, destination *net.IPAddr) (<-chan Reply, error) {\n\treq := multiRequest{}\n\n\tidseq, err := pinger.sendRequest(destination, &req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tgo func() {\n\t\t<-ctx.Done()\n\n\t\t// dequeue request\n\t\tpinger.removeRequest(idseq)\n\n\t\treq.close()\n\t}()\n\n\treturn req.replies, nil\n}\n\n// sendRequest marshals the payload and sends the packet.\n// It returns the combined id+sequence number and an error if the sending failed.\nfunc (pinger *Pinger) sendRequest(destination *net.IPAddr, req request) (uint32, error) {\n\tid := uint16(pinger.Id)\n\tseq := uint16(atomic.AddUint32(pinger.SequenceCounter, 1))\n\n\tidseq := (uint32(id) << 16) | uint32(seq)\n\n\tpinger.payloadMu.RLock()\n\tdefer pinger.payloadMu.RUnlock()\n\n\t// build packet\n\twm := icmp.Message{\n\t\tCode: 0,\n\t\tBody: &icmp.Echo{\n\t\t\tID:   int(id),\n\t\t\tSeq:  int(seq),\n\t\t\tData: pinger.payload,\n\t\t},\n\t}\n\n\t// Protocol specifics\n\tvar conn net.PacketConn\n\tvar lock *sync.Mutex\n\tif destination.IP.To4() != nil {\n\t\twm.Type = ipv4.ICMPTypeEcho\n\t\tconn = pinger.conn4\n\t\tlock = &pinger.write4\n\t} else {\n\t\twm.Type = ipv6.ICMPTypeEchoRequest\n\t\tconn = pinger.conn6\n\t\tlock = &pinger.write6\n\t}\n\n\t// serialize packet\n\twb, err := wm.Marshal(nil)\n\tif err != nil {\n\t\treturn idseq, err\n\t}\n\n\t// enqueue in currently running requests\n\tpinger.mtx.Lock()\n\tpinger.requests[idseq] = req\n\tpinger.mtx.Unlock()\n\n\t// start measurement (tStop is set in the receiving end)\n\tlock.Lock()\n\treq.init()\n\n\t// send request\n\t_, err = conn.WriteTo(wb, destination)\n\tlock.Unlock()\n\n\t// send failed, need to remove request from list\n\tif err != nil {\n\t\treq.close()\n\t\tpinger.removeRequest(idseq)\n\n\t\treturn idseq, err\n\t}\n\n\treturn idseq, nil\n}\n"
  }
]