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
[](https://godoc.org/github.com/digineo/go-ping)
[](https://github.com/digineo/go-ping/actions)
[](https://codecov.io/gh/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
<https://www.digineo.de>
================================================
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/):

## 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<len(res)",
results: []time.Duration{ms, ms, 5 * ms},
received: 2,
last: ms, best: ms, worst: ms, mean: ms, stddev: z,
},
{
title: "different numbers, manually calculated",
results: []time.Duration{ms, 2 * ms},
received: 2,
last: 2 * ms,
best: ms,
worst: 2 * ms,
mean: 1500 * µs,
stddev: 500 * µs,
},
{
title: "wilder numbers",
results: []time.Duration{6 * ms, 2 * ms, 14 * ms, 11 * ms},
received: 6,
lost: 2,
last: 2 * ms, // res[received%len]
best: 2 * ms,
worst: 14 * ms,
mean: 8250 * µs, // (6000+2000+14000+11000)/4
stddev: 4602988, // 4602988.15988
loss: 0.25, // sent = 6+2
},
{
title: "verifying captured data",
received: 50,
lost: 7,
loss: 0.1228, // 7 / 57
last: 488619758,
best: 451327200,
worst: 492082650,
mean: 487287379,
stddev: 9356133,
results: []time.Duration{
478427841, 486727913, 489902185, 490369676, 489957386,
490784152, 491390728, 491012043, 491313203, 489869560,
488634310, 451590351, 480933928, 451431418, 491046095,
492017348, 488906398, 490187284, 490733777, 490418928,
490627269, 490710944, 491339118, 491300740, 490320794,
489706066, 487735713, 488153523, 490988560, 490293234,
492082650, 490784586, 488731408, 488008147, 487630508,
490190288, 490712289, 489931645, 490608008, 490625639,
491721463, 451327200, 491615584, 490238328, 489234608,
488510694, 488807517, 489176334, 488981822, 488619758,
},
},
}
for i, tc := range testcases {
h := history{received: tc.received, results: tc.results, lost: tc.lost}
subject := h.compute()
assert.Equal(tc.best, subject.best, "test case #%d (%s): best", i, tc.title)
assert.Equal(tc.last, subject.last, "test case #%d (%s): last", i, tc.title)
assert.Equal(tc.worst, subject.worst, "test case #%d (%s): worst", i, tc.title)
assert.Equal(tc.mean, subject.mean, "test case #%d (%s): mean", i, tc.title)
assert.Equal(tc.stddev, subject.stddev, "test case #%d (%s): stddev", i, tc.title)
assert.Equal(tc.received+tc.lost, subject.pktSent, "test case #%d (%s): pktSent", i, tc.title)
assert.InDelta(tc.loss, subject.pktLoss, 0.0001, "test case #%d (%s): pktLoss", i, tc.title)
}
}
================================================
FILE: cmd/multiping/main.go
================================================
package main
import (
"flag"
"fmt"
"log"
"os"
"time"
ping "github.com/digineo/go-ping"
)
var opts = struct {
timeout time.Duration
interval time.Duration
payloadSize uint
statBufferSize uint
bind4 string
bind6 string
dests []*destination
resolverTimeout time.Duration
}{
timeout: 1000 * time.Millisecond,
interval: 1000 * time.Millisecond,
bind4: "0.0.0.0",
bind6: "::",
payloadSize: 56,
statBufferSize: 50,
resolverTimeout: 1500 * time.Millisecond,
}
var (
pinger *ping.Pinger
tui *userInterface
)
func main() {
flag.Usage = func() {
fmt.Fprintln(os.Stderr, "Usage:", os.Args[0], "[options] host [host [...]]")
flag.PrintDefaults()
}
flag.DurationVar(&opts.timeout, "timeout", opts.timeout, "timeout for a single echo request")
flag.DurationVar(&opts.interval, "interval", opts.interval, "polling interval")
flag.UintVar(&opts.payloadSize, "s", opts.payloadSize, "size of payload in bytes")
flag.UintVar(&opts.statBufferSize, "buf", opts.statBufferSize, "buffer size for statistics")
flag.StringVar(&opts.bind4, "bind4", opts.bind4, "IPv4 bind address")
flag.StringVar(&opts.bind6, "bind6", opts.bind6, "IPv6 bind address")
flag.DurationVar(&opts.resolverTimeout, "resolve", opts.resolverTimeout, "timeout for DNS lookups")
flag.Parse()
log.SetFlags(0)
for _, host := range flag.Args() {
remotes, err := resolve(host, opts.resolverTimeout)
if err != nil {
log.Printf("error resolving host %s: %v", host, err)
continue
}
for _, remote := range remotes {
if v4 := remote.IP.To4() != nil; v4 && opts.bind4 == "" || !v4 && opts.bind6 == "" {
continue
}
ipaddr := remote // need to create a copy
dst := destination{
host: host,
remote: &ipaddr,
history: &history{
results: make([]time.Duration, opts.statBufferSize),
},
}
opts.dests = append(opts.dests, &dst)
}
}
if instance, err := ping.New(opts.bind4, opts.bind6); err == nil {
if instance.PayloadSize() != uint16(opts.payloadSize) {
instance.SetPayloadSize(uint16(opts.payloadSize))
}
pinger = instance
defer pinger.Close()
} else {
panic(err)
}
go work()
tui = buildTUI(opts.dests)
go tui.update(time.Second)
if err := tui.Run(); err != nil {
panic(err)
}
}
func work() {
for {
for i, u := range opts.dests {
go func(u *destination, i int) {
u.ping(pinger)
}(u, i)
}
time.Sleep(opts.interval)
}
}
================================================
FILE: cmd/multiping/resolve.go
================================================
package main
import (
"context"
"net"
"strings"
"time"
)
func resolve(addr string, timeout time.Duration) ([]net.IPAddr, error) {
if strings.ContainsRune(addr, '%') {
ipaddr, err := net.ResolveIPAddr("ip", addr)
if err != nil {
return nil, err
}
return []net.IPAddr{*ipaddr}, nil
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return net.DefaultResolver.LookupIPAddr(ctx, addr)
}
================================================
FILE: cmd/multiping/ui.go
================================================
package main
import (
"fmt"
"log"
"strconv"
"time"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
type userInterface struct {
app *tview.Application
grid *tview.Grid
table *tview.Table
destinations []*destination
}
var coldef = [...]struct {
title string
align int
initVal func(*destination) string
content func(*stat) string
}{
{
title: "host",
align: tview.AlignLeft,
initVal: func(d *destination) string { return d.host },
},
{
title: "address",
align: tview.AlignLeft,
initVal: func(d *destination) string { return d.remote.IP.String() },
},
{
title: "sent",
align: tview.AlignRight,
content: func(st *stat) string { return strconv.Itoa(st.pktSent) },
},
{
title: "loss",
align: tview.AlignRight,
content: func(st *stat) string { return fmt.Sprintf("%0.1f%%", st.pktLoss*100) },
},
{
title: "last",
align: tview.AlignRight,
content: func(st *stat) string { return ts(st.last) },
},
{
title: "best",
align: tview.AlignRight,
content: func(st *stat) string { return ts(st.best) },
},
{
title: "worst",
align: tview.AlignRight,
content: func(st *stat) string { return ts(st.worst) },
},
{
title: "mean",
align: tview.AlignRight,
content: func(st *stat) string { return ts(st.mean) },
},
{
title: "stddev",
align: tview.AlignRight,
content: func(st *stat) string { return ts(st.stddev) },
},
}
func buildTUI(destinations []*destination) *userInterface {
ui := &userInterface{
app: tview.NewApplication(),
table: tview.NewTable().SetBorders(false).SetFixed(2, 0),
grid: tview.NewGrid().SetRows(3, 0, 10).SetColumns(0),
destinations: destinations,
}
title := tview.NewTextView().
SetDynamicColors(true).
SetTextAlign(tview.AlignCenter).
SetText("[yellow]multiping[white] press q to exit")
logs := tview.NewTextView().
SetDynamicColors(true).
SetWrap(false)
log.SetFlags(log.Ltime | log.LUTC)
log.SetOutput(logs)
ui.grid.AddItem(title, 0, 0, 1, 1, 0, 0, false)
ui.grid.AddItem(ui.table, 1, 0, 1, 1, 0, 0, true)
ui.grid.AddItem(logs, 2, 0, 1, 1, 0, 0, false)
// setup controls
ui.app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Key() {
case tcell.KeyEscape, tcell.KeyCtrlC:
ui.app.Stop()
return nil
case tcell.KeyRune:
if event.Rune() == 'q' {
ui.app.Stop()
return nil
}
}
return event
})
// build header
for col, def := range coldef {
cell := tview.NewTableCell(def.title).SetAlign(def.align)
if col == 2 {
cell.SetExpansion(1)
}
ui.table.SetCell(0, col, cell)
}
// prepare data list
for r, dst := range destinations {
for c, def := range coldef {
var cell *tview.TableCell
if def.initVal != nil {
cell = tview.NewTableCell(def.initVal(dst))
} else {
cell = tview.NewTableCell("n/a")
}
ui.table.SetCell(r+2, c, cell.SetAlign(def.align))
}
}
return ui
}
func (ui *userInterface) Run() error {
ui.app.SetRoot(ui.grid, true).SetFocus(ui.table)
return ui.app.Run()
}
func (ui *userInterface) update(interval time.Duration) {
time.Sleep(interval)
for {
for i, u := range ui.destinations {
stats := u.compute()
r := i + 2
for col, def := range coldef {
if def.content != nil {
ui.table.GetCell(r, col).SetText(def.content(&stats))
}
}
}
ui.app.Draw()
time.Sleep(interval)
}
}
const tsDividend = float64(time.Millisecond) / float64(time.Nanosecond)
func ts(dur time.Duration) string {
if 10*time.Microsecond < dur && dur < time.Second {
return fmt.Sprintf("%0.2f", float64(dur.Nanoseconds())/tsDividend)
}
return dur.String()
}
================================================
FILE: cmd/ping-monitor/Makefile
================================================
TARGET = ping-monitor
include ../common.mk
================================================
FILE: cmd/ping-monitor/main.go
================================================
package main
import (
"flag"
"fmt"
"net"
"os"
"os/signal"
"syscall"
"time"
"github.com/digineo/go-ping"
"github.com/digineo/go-ping/monitor"
)
var (
pingInterval = 5 * time.Second
pingTimeout = 4 * time.Second
reportInterval = 60 * time.Second
size uint = 56
pinger *ping.Pinger
targets []string
)
func main() {
flag.Usage = func() {
fmt.Fprintln(os.Stderr, "Usage:", os.Args[0], "[options] host [host [...]]")
flag.PrintDefaults()
}
flag.DurationVar(&pingInterval, "pingInterval", pingInterval, "interval for ICMP echo requests")
flag.DurationVar(&pingTimeout, "pingTimeout", pingTimeout, "timeout for ICMP echo request")
flag.DurationVar(&reportInterval, "reportInterval", reportInterval, "interval for reports")
flag.UintVar(&size, "size", size, "size of additional payload data")
flag.Parse()
if n := flag.NArg(); n == 0 {
// Targets empty?
flag.Usage()
os.Exit(1)
} else if n > 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
}
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
SYMBOL INDEX (96 symbols across 22 files)
FILE: cmd/multiping/destination.go
type history (line 13) | type history struct
method addResult (line 44) | func (s *history) addResult(rtt time.Duration, err error) {
method compute (line 55) | func (s *history) compute() (st stat) {
type destination (line 20) | type destination struct
method ping (line 36) | func (u *destination) ping(pinger *ping.Pinger) {
type stat (line 26) | type stat struct
FILE: cmd/multiping/destination_test.go
function TestComputeStats (line 10) | func TestComputeStats(t *testing.T) {
FILE: cmd/multiping/main.go
function main (line 37) | func main() {
function work (line 99) | func work() {
FILE: cmd/multiping/resolve.go
function resolve (line 10) | func resolve(addr string, timeout time.Duration) ([]net.IPAddr, error) {
FILE: cmd/multiping/ui.go
type userInterface (line 13) | type userInterface struct
method Run (line 136) | func (ui *userInterface) Run() error {
method update (line 141) | func (ui *userInterface) update(interval time.Duration) {
function buildTUI (line 73) | func buildTUI(destinations []*destination) *userInterface {
constant tsDividend (line 159) | tsDividend = float64(time.Millisecond) / float64(time.Nanosecond)
function ts (line 161) | func ts(dur time.Duration) string {
FILE: cmd/ping-monitor/main.go
function main (line 25) | func main() {
FILE: cmd/ping-test/main.go
function main (line 27) | func main() {
function unicastPing (line 94) | func unicastPing() {
function multicastPing (line 105) | func multicastPing() {
FILE: cmd/pingnet/main.go
type workGenerator (line 33) | type workGenerator struct
method size (line 38) | func (w *workGenerator) size() uint64 {
method each (line 43) | func (w *workGenerator) each(callback func(net.IP) error) error {
type result (line 64) | type result struct
function main (line 70) | func main() {
FILE: error.go
type timeoutError (line 12) | type timeoutError struct
method Error (line 14) | func (e *timeoutError) Error() string { return "i/o timeout" }
method Timeout (line 15) | func (e *timeoutError) Timeout() bool { return true }
method Temporary (line 16) | func (e *timeoutError) Temporary() bool { return true }
FILE: monitor/history.go
type Result (line 12) | type Result struct
type History (line 18) | type History struct
method AddResult (line 33) | func (h *History) AddResult(rtt time.Duration, err error) {
method clear (line 46) | func (h *History) clear() {
method ComputeAndClear (line 52) | func (h *History) ComputeAndClear() *Metrics {
method Compute (line 61) | func (h *History) Compute() *Metrics {
method compute (line 67) | func (h *History) compute() *Metrics {
function NewHistory (line 26) | func NewHistory(capacity int) History {
FILE: monitor/history_test.go
function BenchmarkAddResult (line 12) | func BenchmarkAddResult(b *testing.B) {
function BenchmarkCompute (line 19) | func BenchmarkCompute(b *testing.B) {
function TestCompute (line 27) | func TestCompute(t *testing.T) {
function TestHistoryCapacity (line 118) | func TestHistoryCapacity(t *testing.T) {
FILE: monitor/metrics.go
type Metrics (line 4) | type Metrics struct
FILE: monitor/monitor.go
type Monitor (line 12) | type Monitor struct
method Stop (line 37) | func (p *Monitor) Stop() {
method AddTarget (line 49) | func (p *Monitor) AddTarget(key string, addr net.IPAddr) (err error) {
method AddTargetDelayed (line 54) | func (p *Monitor) AddTargetDelayed(key string, addr net.IPAddr, startu...
method RemoveTarget (line 68) | func (p *Monitor) RemoveTarget(key string) {
method removeTarget (line 76) | func (p *Monitor) removeTarget(key string) {
method ExportAndClear (line 87) | func (p *Monitor) ExportAndClear() map[string]*Metrics {
method Export (line 92) | func (p *Monitor) Export() map[string]*Metrics {
method export (line 96) | func (p *Monitor) export(clear bool) map[string]*Metrics {
constant defaultHistorySize (line 22) | defaultHistorySize = 10
function New (line 26) | func New(pinger *ping.Pinger, interval, timeout time.Duration) *Monitor {
FILE: monitor/target.go
type Target (line 12) | type Target struct
method run (line 37) | func (n *Target) run(startupDelay time.Duration) {
method Stop (line 59) | func (n *Target) Stop() {
method Compute (line 65) | func (n *Target) Compute(clear bool) *Metrics {
method ping (line 72) | func (n *Target) ping() {
function newTarget (line 23) | func newTarget(interval, timeout, startupDelay time.Duration, historySiz...
FILE: payload.go
type Payload (line 23) | type Payload
method Resize (line 26) | func (p *Payload) Resize(size uint16) {
FILE: pinger.go
constant ProtocolICMP (line 14) | ProtocolICMP = 1
constant ProtocolICMPv6 (line 18) | ProtocolICMPv6 = 58
type Pinger (line 25) | type Pinger struct
method Close (line 85) | func (pinger *Pinger) Close() {
method close (line 100) | func (pinger *Pinger) close(conn net.PacketConn) {
method removeRequest (line 106) | func (pinger *Pinger) removeRequest(idseq uint32) {
method SetPayloadSize (line 116) | func (pinger *Pinger) SetPayloadSize(size uint16) {
method SetPayload (line 123) | func (pinger *Pinger) SetPayload(data []byte) {
method PayloadSize (line 130) | func (pinger *Pinger) PayloadSize() uint16 {
function New (line 44) | func New(bind4, bind6 string) (*Pinger, error) {
function connectICMP (line 92) | func connectICMP(network, address string) (*icmp.PacketConn, error) {
FILE: pinger_linux.go
function getFD (line 13) | func getFD(c *icmp.PacketConn) (uintptr, error) {
method SetMark (line 32) | func (pinger *Pinger) SetMark(mark uint) error {
FILE: pinger_other.go
method SetMark (line 7) | func (pinger *Pinger) SetMark(mark uint) error {
FILE: pinger_test.go
function TestPinger (line 12) | func TestPinger(t *testing.T) {
FILE: receiving.go
method receiver (line 15) | func (pinger *Pinger) receiver(proto int, conn net.PacketConn) {
method receive (line 42) | func (pinger *Pinger) receive(proto int, bytes []byte, addr net.IP, t ti...
method process (line 92) | func (pinger *Pinger) process(body icmp.MessageBody, result error, addr ...
FILE: request.go
type request (line 9) | type request interface
type multiRequest (line 16) | type multiRequest struct
method init (line 74) | func (req *multiRequest) init() {
method close (line 79) | func (req *multiRequest) close() {
method handleReply (line 87) | func (req *multiRequest) handleReply(err error, addr net.IP, tRecv *ti...
type Reply (line 24) | type Reply struct
type simpleRequest (line 30) | type simpleRequest struct
method handleReply (line 39) | func (req *simpleRequest) handleReply(err error, _ net.IP, tRecv *time...
method init (line 49) | func (req *simpleRequest) init() {
method close (line 54) | func (req *simpleRequest) close() {
method roundTripTime (line 64) | func (req *simpleRequest) roundTripTime() (time.Duration, error) {
FILE: sending.go
method PingAttempts (line 18) | func (pinger *Pinger) PingAttempts(destination *net.IPAddr, timeout time...
method Ping (line 34) | func (pinger *Pinger) Ping(destination *net.IPAddr, timeout time.Duratio...
method PingContext (line 42) | func (pinger *Pinger) PingContext(ctx context.Context, destination *net....
method PingMulticast (line 70) | func (pinger *Pinger) PingMulticast(destination *net.IPAddr, wait time.D...
method PingMulticastContext (line 77) | func (pinger *Pinger) PingMulticastContext(ctx context.Context, destinat...
method sendRequest (line 99) | func (pinger *Pinger) sendRequest(destination *net.IPAddr, req request) ...
Condensed preview — 39 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (71K chars).
[
{
"path": ".codecov.yml",
"chars": 14,
"preview": "ignore:\n- cmd\n"
},
{
"path": ".github/build-all",
"chars": 87,
"preview": "#!/bin/sh -e\n\nfor mf in $(find cmd -name Makefile); do\n make -C \"$(dirname $mf)\"\ndone\n"
},
{
"path": ".github/workflows/build.yml",
"chars": 562,
"preview": "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"
},
{
"path": ".github/workflows/golangci-lint.yml",
"chars": 344,
"preview": "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 "
},
{
"path": ".gitignore",
"chars": 448,
"preview": "# 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"
},
{
"path": ".golangci.yml",
"chars": 405,
"preview": "version: \"2\"\nlinters:\n exclusions:\n generated: lax\n presets:\n - comments\n - common-false-positives\n "
},
{
"path": "LICENSE",
"chars": 1069,
"preview": "MIT License\n\nCopyright (c) 2018 Digineo GmbH\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
},
{
"path": "README.md",
"chars": 1898,
"preview": "# go-ping\n\n[](https://godoc.org/github.com/digineo/go-p"
},
{
"path": "cmd/common.mk",
"chars": 108,
"preview": ".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",
"chars": 154,
"preview": "TARGET = multiping\n\ninclude ../common.mk\n\n.PHONY: test\ntest: all\n\tgo test ./...\n\t./$(TARGET) golang.org cloudflare.com 2"
},
{
"path": "cmd/multiping/README.md",
"chars": 1982,
"preview": "# multiping\n\nJust like regular `ping`, but measures round trip time to multiple\nhosts, all at once.\n\nThe user interface "
},
{
"path": "cmd/multiping/destination.go",
"chars": 1812,
"preview": "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 "
},
{
"path": "cmd/multiping/destination_test.go",
"chars": 3302,
"preview": "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) "
},
{
"path": "cmd/multiping/main.go",
"chars": 2499,
"preview": "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\tt"
},
{
"path": "cmd/multiping/resolve.go",
"chars": 440,
"preview": "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.I"
},
{
"path": "cmd/multiping/ui.go",
"chars": 3699,
"preview": "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"
},
{
"path": "cmd/ping-monitor/Makefile",
"chars": 44,
"preview": "TARGET = ping-monitor\n\ninclude ../common.mk\n"
},
{
"path": "cmd/ping-monitor/main.go",
"chars": 2104,
"preview": "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\"gith"
},
{
"path": "cmd/ping-test/Makefile",
"chars": 333,
"preview": "TARGET = ping-test\n\ninclude ../common.mk\n\n.PHONY: test\ntest: all\n\t@truncate -s0 $(TARGET).log\n\t@./$(TARGET) -4 golang.or"
},
{
"path": "cmd/ping-test/README.md",
"chars": 1642,
"preview": "# ping-test\n\nThis is a sample program to demonstrate the usage of this library.\nIt is not intended for production use.\n\n"
},
{
"path": "cmd/ping-test/main.go",
"chars": 2344,
"preview": "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 "
},
{
"path": "cmd/pingnet/Makefile",
"chars": 106,
"preview": "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",
"chars": 4489,
"preview": "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/digin"
},
{
"path": "error.go",
"chars": 505,
"preview": "package ping\n\nimport \"errors\"\n\nvar (\n\terrClosed = errors.New(\"pinger closed\")\n\terrNotBound = errors.New(\"need at least"
},
{
"path": "go.mod",
"chars": 915,
"preview": "module github.com/digineo/go-ping\n\ngo 1.23.0\n\nrequire (\n\tgithub.com/digineo/go-logwrap v0.0.0-20181106161722-a178c58ea3f"
},
{
"path": "go.sum",
"chars": 6667,
"preview": "github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.m"
},
{
"path": "monitor/history.go",
"chars": 2503,
"preview": "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 pa"
},
{
"path": "monitor/history_test.go",
"chars": 3466,
"preview": "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 BenchmarkAddR"
},
{
"path": "monitor/metrics.go",
"chars": 406,
"preview": "package monitor\n\n// Metrics is a dumb data point computed from a history of Results.\ntype Metrics struct {\n\tPacketsSent "
},
{
"path": "monitor/monitor.go",
"chars": 2705,
"preview": "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 res"
},
{
"path": "monitor/target.go",
"chars": 1413,
"preview": "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"
},
{
"path": "payload.go",
"chars": 738,
"preview": "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// S"
},
{
"path": "pinger.go",
"chars": 3242,
"preview": "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 I"
},
{
"path": "pinger_linux.go",
"chars": 1392,
"preview": "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 s"
},
{
"path": "pinger_other.go",
"chars": 196,
"preview": "//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"
},
{
"path": "pinger_test.go",
"chars": 529,
"preview": "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/re"
},
{
"path": "receiving.go",
"chars": 2865,
"preview": "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\""
},
{
"path": "request.go",
"chars": 2204,
"preview": "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, *"
},
{
"path": "sending.go",
"chars": 3969,
"preview": "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"
}
]
About this extraction
This page contains the full source code of the digineo/go-ping GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 39 files (62.1 KB), approximately 21.4k tokens, and a symbol index with 96 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.