Update dependencies

This commit is contained in:
bluepython508
2024-11-01 17:33:34 +00:00
parent 033ac0b400
commit 5cdfab398d
3596 changed files with 1033483 additions and 259 deletions

View File

@@ -0,0 +1,16 @@
# https://editorconfig.org
root = true
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
indent_style = space
[Makefile]
indent_style = tab
[*.go]
indent_style = tab

View File

@@ -0,0 +1,2 @@
/ping
/dist

View File

@@ -0,0 +1,19 @@
---
linters:
enable:
- misspell
- revive
issues:
exclude-rules:
- path: _test.go
linters:
- errcheck
linters-settings:
revive:
rules:
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-parameter
- name: unused-parameter
severity: warning
disabled: true

View File

@@ -0,0 +1,46 @@
project_name: ping
before:
hooks:
- go mod download
builds:
- binary: ping
dir: cmd/ping
goarch:
- amd64
- arm
- arm64
goarm:
- 6
- 7
goos:
- darwin
- freebsd
- linux
- windows
archives:
- files:
- LICENSE
- README.md
format_overrides:
- goos: windows
format: zip
wrap_in_directory: true
# TODO: Decide if we want packages (name conflcits with /bin/ping?)
# nfpms:
# homepage: https://github.com/go-ping/ping
# maintainer: 'Go Ping Maintainers <go-ping@example.com>'
# description: Ping written in Go.
# license: MIT
# formats:
# - deb
# - rpm
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ .Tag }}-{{ .ShortCommit }}"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'

View File

@@ -0,0 +1,3 @@
# Prometheus Community Code of Conduct
Prometheus follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md).

View File

@@ -0,0 +1,44 @@
# Contributing
First off, thanks for taking the time to contribute!
Remember that this is open source software so please consider the other people who will read your code.
Make it look nice for them, document your logic in comments and add or update the unit test cases.
This library is used by various other projects, companies and individuals in live production environments so please discuss any breaking changes with us before making them.
Feel free to join us in the [#pro-bing](https://gophers.slack.com/archives/C019J5E26U8/p1673599762771949) channel of the [Gophers Slack](https://invite.slack.golangbridge.org/).
## Pull Requests
[Fork the repo on GitHub](https://github.com/prometheus-community/pro-bing/fork) and clone it to your local machine.
```bash
git clone https://github.com/YOUR_USERNAME/pro-bing.git && cd pro-bing
```
Here is a guide on [how to configure a remote repository](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/configuring-a-remote-for-a-fork).
Check out a new branch, make changes, run tests, commit & sign-off, then push branch to your fork.
```bash
$ git checkout -b <BRANCH_NAME>
# edit files
$ make style vet test
$ git add <CHANGED_FILES>
$ git commit -s
$ git push <FORK> <BRANCH_NAME>
```
Open a [new pull request](https://github.com/prometheus-community/pro-bing/compare) in the main `prometheus-community/pro-bing` repository.
Please describe the purpose of your PR and remember link it to any related issues.
*We may ask you to rebase your feature branch or squash the commits in order to keep the history clean.*
## Development Guides
- Run `make style vet test` before committing your changes.
- Document your logic in code comments.
- Add tests for bug fixes and new features.
- Use UNIX-style (LF) line endings.
- End every file with a single blank line.
- Use the UTF-8 character set.

View File

@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright 2022 The Prometheus Authors
Copyright 2016 Cameron Sparr and contributors.
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.

View File

@@ -0,0 +1,4 @@
# Maintainers
* Ben Kochie <superq@gmail.com> @SuperQ
* Matthias Loibl <mail@matthiasloibl.com> @metalmatze

View File

@@ -0,0 +1,32 @@
GO ?= go
GOFMT ?= $(GO)fmt
GOOPTS ?=
GO111MODULE :=
pkgs = ./...
all: style vet build test
.PHONY: build
build:
@echo ">> building ping"
GO111MODULE=$(GO111MODULE) $(GO) build $(GOOPTS) ./cmd/ping
.PHONY: style
style:
@echo ">> checking code style"
@fmtRes=$$($(GOFMT) -d $$(find . -path ./vendor -prune -o -name '*.go' -print)); \
if [ -n "$${fmtRes}" ]; then \
echo "gofmt checking failed!"; echo "$${fmtRes}"; echo; \
echo "Please ensure you are using $$($(GO) version) for formatting code."; \
exit 1; \
fi
.PHONY: test
test:
@echo ">> running all tests"
GO111MODULE=$(GO111MODULE) $(GO) test -race -cover $(GOOPTS) $(pkgs)
.PHONY: vet
vet:
@echo ">> vetting code"
GO111MODULE=$(GO111MODULE) $(GO) vet $(GOOPTS) $(pkgs)

View File

@@ -0,0 +1,191 @@
# pro-bing
[![PkgGoDev](https://pkg.go.dev/badge/github.com/prometheus-community/pro-bing)](https://pkg.go.dev/github.com/prometheus-community/pro-bing)
[![Circle CI](https://circleci.com/gh/prometheus-community/pro-bing.svg?style=svg)](https://circleci.com/gh/prometheus-community/pro-bing)
A simple but powerful ICMP echo (ping) library for Go, inspired by
[go-ping](https://github.com/go-ping/ping) & [go-fastping](https://github.com/tatsushid/go-fastping).
Here is a very simple example that sends and receives three packets:
```go
pinger, err := probing.NewPinger("www.google.com")
if err != nil {
panic(err)
}
pinger.Count = 3
err = pinger.Run() // Blocks until finished.
if err != nil {
panic(err)
}
stats := pinger.Statistics() // get send/receive/duplicate/rtt stats
```
Here is an example that emulates the traditional UNIX ping command:
```go
pinger, err := probing.NewPinger("www.google.com")
if err != nil {
panic(err)
}
// Listen for Ctrl-C.
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
for _ = range c {
pinger.Stop()
}
}()
pinger.OnRecv = func(pkt *probing.Packet) {
fmt.Printf("%d bytes from %s: icmp_seq=%d time=%v\n",
pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt)
}
pinger.OnDuplicateRecv = func(pkt *probing.Packet) {
fmt.Printf("%d bytes from %s: icmp_seq=%d time=%v ttl=%v (DUP!)\n",
pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt, pkt.TTL)
}
pinger.OnFinish = func(stats *probing.Statistics) {
fmt.Printf("\n--- %s ping statistics ---\n", stats.Addr)
fmt.Printf("%d packets transmitted, %d packets received, %v%% packet loss\n",
stats.PacketsSent, stats.PacketsRecv, stats.PacketLoss)
fmt.Printf("round-trip min/avg/max/stddev = %v/%v/%v/%v\n",
stats.MinRtt, stats.AvgRtt, stats.MaxRtt, stats.StdDevRtt)
}
fmt.Printf("PING %s (%s):\n", pinger.Addr(), pinger.IPAddr())
err = pinger.Run()
if err != nil {
panic(err)
}
```
It sends ICMP Echo Request packet(s) and waits for an Echo Reply in
response. If it receives a response, it calls the `OnRecv` callback
unless a packet with that sequence number has already been received,
in which case it calls the `OnDuplicateRecv` callback. When it's
finished, it calls the `OnFinish` callback.
For a full ping example, see
[cmd/ping/ping.go](https://github.com/prometheus-community/pro-bing/blob/master/cmd/ping/ping.go).
## Installation
```
go get -u github.com/prometheus-community/pro-bing
```
To install the native Go ping executable:
```bash
go get -u github.com/prometheus-community/pro-bing/...
$GOPATH/bin/ping
```
## Supported Operating Systems
### Linux
This library attempts to send an "unprivileged" ping via UDP. On Linux,
this must be enabled with the following sysctl command:
```
sudo sysctl -w net.ipv4.ping_group_range="0 2147483647"
```
If you do not wish to do this, you can call `pinger.SetPrivileged(true)`
in your code and then use setcap on your binary to allow it to bind to
raw sockets (or just run it as root):
```
setcap cap_net_raw=+ep /path/to/your/compiled/binary
```
See [this blog](https://sturmflut.github.io/linux/ubuntu/2015/01/17/unprivileged-icmp-sockets-on-linux/)
and the Go [x/net/icmp](https://godoc.org/golang.org/x/net/icmp) package
for more details.
This library supports setting the `SO_MARK` socket option which is equivalent to the `-m mark`
flag in standard ping binaries on linux. Setting this option requires the `CAP_NET_ADMIN` capability
(via `setcap` or elevated privileges). You can set a mark (ex: 100) with `pinger.SetMark(100)` in your code.
Setting the "Don't Fragment" bit is supported under Linux which is equivalent to `ping -Mdo`.
You can enable this with `pinger.SetDoNotFragment(true)`.
### Windows
You must use `pinger.SetPrivileged(true)`, otherwise you will receive
the following error:
```
socket: The requested protocol has not been configured into the system, or no implementation for it exists.
```
Despite the method name, this should work without the need to elevate
privileges and has been tested on Windows 10. Please note that accessing
packet TTL values is not supported due to limitations in the Go
x/net/ipv4 and x/net/ipv6 packages.
### Plan 9 from Bell Labs
There is no support for Plan 9. This is because the entire `x/net/ipv4`
and `x/net/ipv6` packages are not implemented by the Go programming
language.
## HTTP
This library also provides support for HTTP probing.
Here is a trivial example:
```go
httpCaller := probing.NewHttpCaller("https://www.google.com",
probing.WithHTTPCallerCallFrequency(time.Second),
probing.WithHTTPCallerOnResp(func(suite *probing.TraceSuite, info *probing.HTTPCallInfo) {
fmt.Printf("got resp, status code: %d, latency: %s\n",
info.StatusCode,
suite.GetGeneralEnd().Sub(suite.GetGeneralStart()),
)
}),
)
// Listen for Ctrl-C.
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
<-c
httpCaller.Stop()
}()
httpCaller.Run()
```
Library provides a rich list of options available for a probing. You can check the full list of available
options in a generated doc.
### Callbacks
HTTPCaller uses `net/http/httptrace` pkg to provide an API to track specific request event, e.g. tls handshake start.
It is highly recommended to check the httptrace library [doc](https://pkg.go.dev/net/http/httptrace) to understand
the purpose of provided callbacks. Nevertheless, httptrace callbacks are concurrent-unsafe, our implementation provides
a concurrent-safe API. In addition to that, each callback contains a TraceSuite object which provides an Extra field
which you can use to propagate your data across them and a number of timer fields, which are set prior to the execution of a
corresponding callback.
### Target RPS & performance
Library provides two options, allowing to manipulate your call load: `callFrequency` & `maxConcurrentCalls`.
In case you set `callFrequency` to a value X, but it can't be achieved during the execution - you will need to
try increasing a number of `maxConcurrentCalls`. Moreover, your callbacks might directly influence an execution
performance.
For a full documentation, please refer to the generated [doc](https://pkg.go.dev/github.com/prometheus-community/pro-bing).
## Maintainers and Getting Help:
This repo was originally in the personal account of
[sparrc](https://github.com/sparrc), but is now maintained by the
[Prometheus Community](https://prometheus.io/community).
## Contributing
Refer to [CONTRIBUTING.md](https://github.com/prometheus-community/pro-bing/blob/master/CONTRIBUTING.md)

View File

@@ -0,0 +1,6 @@
# Reporting a security issue
The Prometheus security policy, including how to report vulnerabilities, can be
found here:
<https://prometheus.io/docs/operating/security/>

619
vendor/github.com/prometheus-community/pro-bing/http.go generated vendored Normal file
View File

@@ -0,0 +1,619 @@
package probing
import (
"bytes"
"context"
"crypto/tls"
"io"
"net/http"
"net/http/httptrace"
"sync"
"time"
)
const (
defaultHTTPCallFrequency = time.Second
defaultHTTPMaxConcurrentCalls = 1
defaultHTTPMethod = http.MethodGet
defaultTimeout = time.Second * 10
)
type httpCallerOptions struct {
client *http.Client
callFrequency time.Duration
maxConcurrentCalls int
host string
headers http.Header
method string
body []byte
timeout time.Duration
isValidResponse func(response *http.Response, body []byte) bool
onDNSStart func(suite *TraceSuite, info httptrace.DNSStartInfo)
onDNSDone func(suite *TraceSuite, info httptrace.DNSDoneInfo)
onConnStart func(suite *TraceSuite, network, addr string)
onConnDone func(suite *TraceSuite, network, addr string, err error)
onTLSStart func(suite *TraceSuite)
onTLSDone func(suite *TraceSuite, state tls.ConnectionState, err error)
onWroteHeaders func(suite *TraceSuite)
onFirstByteReceived func(suite *TraceSuite)
onReq func(suite *TraceSuite)
onResp func(suite *TraceSuite, info *HTTPCallInfo)
logger Logger
}
// HTTPCallerOption represents a function type for a functional parameter passed to a NewHttpCaller constructor.
type HTTPCallerOption func(options *httpCallerOptions)
// WithHTTPCallerClient is a functional parameter for a HTTPCaller which specifies a http.Client.
func WithHTTPCallerClient(client *http.Client) HTTPCallerOption {
return func(options *httpCallerOptions) {
options.client = client
}
}
// WithHTTPCallerCallFrequency is a functional parameter for a HTTPCaller which specifies a call frequency.
// If this option is not provided the default one will be used. You can check default value in const
// defaultHTTPCallFrequency.
func WithHTTPCallerCallFrequency(frequency time.Duration) HTTPCallerOption {
return func(options *httpCallerOptions) {
options.callFrequency = frequency
}
}
// WithHTTPCallerMaxConcurrentCalls is a functional parameter for a HTTPCaller which specifies a number of
// maximum concurrent calls. If this option is not provided the default one will be used. You can check default value in const
// defaultHTTPMaxConcurrentCalls.
func WithHTTPCallerMaxConcurrentCalls(max int) HTTPCallerOption {
return func(options *httpCallerOptions) {
options.maxConcurrentCalls = max
}
}
// WithHTTPCallerHeaders is a functional parameter for a HTTPCaller which specifies headers that should be
// set in request.
// To override a Host header use a WithHTTPCallerHost method.
func WithHTTPCallerHeaders(headers http.Header) HTTPCallerOption {
return func(options *httpCallerOptions) {
options.headers = headers
}
}
// WithHTTPCallerMethod is a functional parameter for a HTTPCaller which specifies a method that should be
// set in request. If this option is not provided the default one will be used. You can check default value in const
// defaultHTTPMethod.
func WithHTTPCallerMethod(method string) HTTPCallerOption {
return func(options *httpCallerOptions) {
options.method = method
}
}
// WithHTTPCallerHost is a functional parameter for a HTTPCaller which allowed to override a host header.
func WithHTTPCallerHost(host string) HTTPCallerOption {
return func(options *httpCallerOptions) {
options.host = host
}
}
// WithHTTPCallerBody is a functional parameter for a HTTPCaller which specifies a body that should be set
// in request.
func WithHTTPCallerBody(body []byte) HTTPCallerOption {
return func(options *httpCallerOptions) {
options.body = body
}
}
// WithHTTPCallerTimeout is a functional parameter for a HTTPCaller which specifies request timeout.
// If this option is not provided the default one will be used. You can check default value in const defaultTimeout.
func WithHTTPCallerTimeout(timeout time.Duration) HTTPCallerOption {
return func(options *httpCallerOptions) {
options.timeout = timeout
}
}
// WithHTTPCallerIsValidResponse is a functional parameter for a HTTPCaller which specifies a function that
// will be used to assess whether a response is valid. If not specified, all responses will be treated as valid.
// You can read more explanation about this parameter in HTTPCaller annotation.
func WithHTTPCallerIsValidResponse(isValid func(response *http.Response, body []byte) bool) HTTPCallerOption {
return func(options *httpCallerOptions) {
options.isValidResponse = isValid
}
}
// WithHTTPCallerOnDNSStart is a functional parameter for a HTTPCaller which specifies a callback that will be
// called when dns resolving starts. You can read more explanation about this parameter in HTTPCaller annotation.
func WithHTTPCallerOnDNSStart(onDNSStart func(suite *TraceSuite, info httptrace.DNSStartInfo)) HTTPCallerOption {
return func(options *httpCallerOptions) {
options.onDNSStart = onDNSStart
}
}
// WithHTTPCallerOnDNSDone is a functional parameter for a HTTPCaller which specifies a callback that will be
// called when dns resolving ended. You can read more explanation about this parameter in HTTPCaller annotation.
func WithHTTPCallerOnDNSDone(onDNSDone func(suite *TraceSuite, info httptrace.DNSDoneInfo)) HTTPCallerOption {
return func(options *httpCallerOptions) {
options.onDNSDone = onDNSDone
}
}
// WithHTTPCallerOnConnStart is a functional parameter for a HTTPCaller which specifies a callback that will be
// called when connection establishment started. You can read more explanation about this parameter in HTTPCaller
// annotation.
func WithHTTPCallerOnConnStart(onConnStart func(suite *TraceSuite, network, addr string)) HTTPCallerOption {
return func(options *httpCallerOptions) {
options.onConnStart = onConnStart
}
}
// WithHTTPCallerOnConnDone is a functional parameter for a HTTPCaller which specifies a callback that will be
// called when connection establishment finished. You can read more explanation about this parameter in HTTPCaller
// annotation.
func WithHTTPCallerOnConnDone(conConnDone func(suite *TraceSuite, network, addr string, err error)) HTTPCallerOption {
return func(options *httpCallerOptions) {
options.onConnDone = conConnDone
}
}
// WithHTTPCallerOnTLSStart is a functional parameter for a HTTPCaller which specifies a callback that will be
// called when tls handshake started. You can read more explanation about this parameter in HTTPCaller annotation.
func WithHTTPCallerOnTLSStart(onTLSStart func(suite *TraceSuite)) HTTPCallerOption {
return func(options *httpCallerOptions) {
options.onTLSStart = onTLSStart
}
}
// WithHTTPCallerOnTLSDone is a functional parameter for a HTTPCaller which specifies a callback that will be
// called when tls handshake ended. You can read more explanation about this parameter in HTTPCaller annotation.
func WithHTTPCallerOnTLSDone(onTLSDone func(suite *TraceSuite, state tls.ConnectionState, err error)) HTTPCallerOption {
return func(options *httpCallerOptions) {
options.onTLSDone = onTLSDone
}
}
// WithHTTPCallerOnWroteRequest is a functional parameter for a HTTPCaller which specifies a callback that will be
// called when request has been written. You can read more explanation about this parameter in HTTPCaller annotation.
func WithHTTPCallerOnWroteRequest(onWroteRequest func(suite *TraceSuite)) HTTPCallerOption {
return func(options *httpCallerOptions) {
options.onWroteHeaders = onWroteRequest
}
}
// WithHTTPCallerOnFirstByteReceived is a functional parameter for a HTTPCaller which specifies a callback that will be
// called when first response byte has been received. You can read more explanation about this parameter in HTTPCaller
// annotation.
func WithHTTPCallerOnFirstByteReceived(onGotFirstByte func(suite *TraceSuite)) HTTPCallerOption {
return func(options *httpCallerOptions) {
options.onFirstByteReceived = onGotFirstByte
}
}
// WithHTTPCallerOnReq is a functional parameter for a HTTPCaller which specifies a callback that will be
// called before the start of the http call execution. You can read more explanation about this parameter in HTTPCaller
// annotation.
func WithHTTPCallerOnReq(onReq func(suite *TraceSuite)) HTTPCallerOption {
return func(options *httpCallerOptions) {
options.onReq = onReq
}
}
// WithHTTPCallerOnResp is a functional parameter for a HTTPCaller which specifies a callback that will be
// called when response is received. You can read more explanation about this parameter in HTTPCaller annotation.
func WithHTTPCallerOnResp(onResp func(suite *TraceSuite, info *HTTPCallInfo)) HTTPCallerOption {
return func(options *httpCallerOptions) {
options.onResp = onResp
}
}
// WithHTTPCallerLogger is a functional parameter for a HTTPCaller which specifies a logger.
// If not specified, logs will be omitted.
func WithHTTPCallerLogger(logger Logger) HTTPCallerOption {
return func(options *httpCallerOptions) {
options.logger = logger
}
}
// NewHttpCaller returns a new HTTPCaller. URL parameter is the only required one, other options might be specified via
// functional parameters, otherwise default values will be used where applicable.
func NewHttpCaller(url string, options ...HTTPCallerOption) *HTTPCaller {
opts := httpCallerOptions{
callFrequency: defaultHTTPCallFrequency,
maxConcurrentCalls: defaultHTTPMaxConcurrentCalls,
method: defaultHTTPMethod,
timeout: defaultTimeout,
client: &http.Client{},
}
for _, opt := range options {
opt(&opts)
}
return &HTTPCaller{
client: opts.client,
callFrequency: opts.callFrequency,
maxConcurrentCalls: opts.maxConcurrentCalls,
url: url,
host: opts.host,
headers: opts.headers,
method: opts.method,
body: opts.body,
timeout: opts.timeout,
isValidResponse: opts.isValidResponse,
workChan: make(chan struct{}, opts.maxConcurrentCalls),
doneChan: make(chan struct{}),
onDNSStart: opts.onDNSStart,
onDNSDone: opts.onDNSDone,
onConnStart: opts.onConnStart,
onConnDone: opts.onConnDone,
onTLSStart: opts.onTLSStart,
onTLSDone: opts.onTLSDone,
onWroteHeaders: opts.onWroteHeaders,
onFirstByteReceived: opts.onFirstByteReceived,
onReq: opts.onReq,
onResp: opts.onResp,
logger: opts.logger,
}
}
// HTTPCaller represents a prober performing http calls and collecting relevant statistics.
type HTTPCaller struct {
client *http.Client
// callFrequency is a parameter which specifies how often to send a new request. You might need to increase
// maxConcurrentCalls value to achieve required value.
callFrequency time.Duration
// maxConcurrentCalls is a maximum number of calls that might be performed concurrently. In other words,
// a number of "workers" that will try to perform probing concurrently.
// Default number is specified in defaultHTTPMaxConcurrentCalls
maxConcurrentCalls int
// url is an url which will be used in all probe requests, mandatory in constructor.
url string
// host allows to override a Host header
host string
// headers are headers that which will be used in all probe requests, default are none.
headers http.Header
// method is a http request method which will be used in all probe requests,
// default is specified in defaultHTTPMethod
method string
// body is a http request body which will be used in all probe requests, default is none.
body []byte
// timeout is a http call timeout, default is specified in defaultTimeout.
timeout time.Duration
// isValidResponse is a function that will be used to validate whether a response is valid up to clients choice.
// You can think of it as a verification that response contains data that you expected. This information will be
// passed back in HTTPCallInfo during an onResp callback and HTTPStatistics during an onFinish callback
// or a Statistics call.
isValidResponse func(response *http.Response, body []byte) bool
workChan chan struct{}
doneChan chan struct{}
doneWg sync.WaitGroup
// All callbacks except onReq and onResp are based on a httptrace callbacks, meaning they are called at the time
// and contain signature same as you would expect in httptrace library. In addition to that each callback has a
// TraceSuite as a first argument, which will help you to propagate data between these callbacks. You can read more
// about it in TraceSuite annotation.
// onDNSStart is a callback which is called when a dns lookup starts. It's based on a httptrace.DNSStart callback.
onDNSStart func(suite *TraceSuite, info httptrace.DNSStartInfo)
// onDNSDone is a callback which is called when a dns lookup ends. It's based on a httptrace.DNSDone callback.
onDNSDone func(suite *TraceSuite, info httptrace.DNSDoneInfo)
// onConnStart is a callback which is called when a connection dial starts. It's based on a httptrace.ConnectStart
// callback.
onConnStart func(suite *TraceSuite, network, addr string)
// onConnDone is a callback which is called when a connection dial ends. It's based on a httptrace.ConnectDone
// callback.
onConnDone func(suite *TraceSuite, network, addr string, err error)
// onTLSStart is a callback which is called when a tls handshake starts. It's based on a httptrace.TLSHandshakeStart
// callback.
onTLSStart func(suite *TraceSuite)
// onTLSDone is a callback which is called when a tls handshake ends. It's based on a httptrace.TLSHandshakeDone
// callback.
onTLSDone func(suite *TraceSuite, state tls.ConnectionState, err error)
// onWroteHeaders is a callback which is called when request headers where written. It's based on a
// httptrace.WroteHeaders callback.
onWroteHeaders func(suite *TraceSuite)
// onFirstByteReceived is a callback which is called when first response bytes were received. It's based on a
// httptrace.GotFirstResponseByte callback.
onFirstByteReceived func(suite *TraceSuite)
// onReq is a custom callback which is called before http client starts request execution.
onReq func(suite *TraceSuite)
// onResp is a custom callback which is called when a response is received.
onResp func(suite *TraceSuite, info *HTTPCallInfo)
// logger is a logger implementation, default is none.
logger Logger
}
// Stop gracefully stops the execution of a HTTPCaller.
func (c *HTTPCaller) Stop() {
close(c.doneChan)
c.doneWg.Wait()
}
// Run starts execution of a probing.
func (c *HTTPCaller) Run() {
c.run(context.Background())
}
// RunWithContext starts execution of a probing and allows providing a context.
func (c *HTTPCaller) RunWithContext(ctx context.Context) {
c.run(ctx)
}
func (c *HTTPCaller) run(ctx context.Context) {
c.runWorkScheduler(ctx)
c.runCallers(ctx)
c.doneWg.Wait()
}
func (c *HTTPCaller) runWorkScheduler(ctx context.Context) {
c.doneWg.Add(1)
go func() {
defer c.doneWg.Done()
ticker := time.NewTicker(c.callFrequency)
defer ticker.Stop()
for {
select {
case <-ticker.C:
c.workChan <- struct{}{}
case <-ctx.Done():
return
case <-c.doneChan:
return
}
}
}()
}
func (c *HTTPCaller) runCallers(ctx context.Context) {
for i := 0; i < c.maxConcurrentCalls; i++ {
c.doneWg.Add(1)
go func() {
defer c.doneWg.Done()
for {
logger := c.logger
if logger == nil {
logger = NoopLogger{}
}
select {
case <-c.workChan:
if err := c.makeCall(ctx); err != nil {
logger.Errorf("failed making a call: %v", err)
}
case <-ctx.Done():
return
case <-c.doneChan:
return
}
}
}()
}
}
// TraceSuite is a struct that is passed to each callback. It contains a bunch of time helpers, that you can use with
// a corresponding getter. These timers are set before making a corresponding callback, meaning that when an onDNSStart
// callback will be called - TraceSuite will already have filled dnsStart field. In addition to that, it contains
// an Extra field of type any which you can use in any custom way you might need. Before each callback call, mutex
// is used, meaning all operations inside your callback are concurrent-safe.
// Keep in mind, that if your http client set up to follow redirects - timers will be overwritten.
type TraceSuite struct {
mu sync.Mutex
generalStart time.Time
generalEnd time.Time
dnsStart time.Time
dnsEnd time.Time
connStart time.Time
connEnd time.Time
tlsStart time.Time
tlsEnd time.Time
wroteHeaders time.Time
firstByteReceived time.Time
Extra any
}
// GetGeneralStart returns a general http request execution start time.
func (s *TraceSuite) GetGeneralStart() time.Time {
return s.generalStart
}
// GetGeneralEnd returns a general http response time.
func (s *TraceSuite) GetGeneralEnd() time.Time {
return s.generalEnd
}
// GetDNSStart returns a time of a dns lookup start.
func (s *TraceSuite) GetDNSStart() time.Time {
return s.dnsStart
}
// GetDNSEnd returns a time of a dns lookup send.
func (s *TraceSuite) GetDNSEnd() time.Time {
return s.dnsEnd
}
// GetConnStart returns a time of a connection dial start.
func (s *TraceSuite) GetConnStart() time.Time {
return s.connStart
}
// GetConnEnd returns a time of a connection dial end.
func (s *TraceSuite) GetConnEnd() time.Time {
return s.connEnd
}
// GetTLSStart returns a time of a tls handshake start.
func (s *TraceSuite) GetTLSStart() time.Time {
return s.tlsStart
}
// GetTLSEnd returns a time of a tls handshake end.
func (s *TraceSuite) GetTLSEnd() time.Time {
return s.tlsEnd
}
// GetWroteHeaders returns a time when request headers were written.
func (s *TraceSuite) GetWroteHeaders() time.Time {
return s.wroteHeaders
}
// GetFirstByteReceived returns a time when first response bytes were received.
func (s *TraceSuite) GetFirstByteReceived() time.Time {
return s.firstByteReceived
}
func (c *HTTPCaller) getClientTrace(suite *TraceSuite) *httptrace.ClientTrace {
return &httptrace.ClientTrace{
DNSStart: func(info httptrace.DNSStartInfo) {
suite.mu.Lock()
defer suite.mu.Unlock()
suite.dnsStart = time.Now()
if c.onDNSStart != nil {
c.onDNSStart(suite, info)
}
},
DNSDone: func(info httptrace.DNSDoneInfo) {
suite.mu.Lock()
defer suite.mu.Unlock()
suite.dnsEnd = time.Now()
if c.onDNSDone != nil {
c.onDNSDone(suite, info)
}
},
ConnectStart: func(network, addr string) {
suite.mu.Lock()
defer suite.mu.Unlock()
suite.connStart = time.Now()
if c.onConnStart != nil {
c.onConnStart(suite, network, addr)
}
},
ConnectDone: func(network, addr string, err error) {
suite.mu.Lock()
defer suite.mu.Unlock()
suite.connEnd = time.Now()
if c.onConnDone != nil {
c.onConnDone(suite, network, addr, err)
}
},
TLSHandshakeStart: func() {
suite.mu.Lock()
defer suite.mu.Unlock()
suite.tlsStart = time.Now()
if c.onTLSStart != nil {
c.onTLSStart(suite)
}
},
TLSHandshakeDone: func(state tls.ConnectionState, err error) {
suite.mu.Lock()
defer suite.mu.Unlock()
suite.tlsEnd = time.Now()
if c.onTLSDone != nil {
c.onTLSDone(suite, state, err)
}
},
WroteHeaders: func() {
suite.mu.Lock()
defer suite.mu.Unlock()
suite.wroteHeaders = time.Now()
if c.onWroteHeaders != nil {
c.onWroteHeaders(suite)
}
},
GotFirstResponseByte: func() {
suite.mu.Lock()
defer suite.mu.Unlock()
suite.firstByteReceived = time.Now()
if c.onFirstByteReceived != nil {
c.onFirstByteReceived(suite)
}
},
}
}
func (c *HTTPCaller) makeCall(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
suite := TraceSuite{
generalStart: time.Now(),
}
traceCtx := httptrace.WithClientTrace(ctx, c.getClientTrace(&suite))
req, err := http.NewRequestWithContext(traceCtx, c.method, c.url, bytes.NewReader(c.body))
if err != nil {
return err
}
req.Header = c.headers
if c.host != "" {
req.Host = c.host
}
if c.onReq != nil {
suite.mu.Lock()
c.onReq(&suite)
suite.mu.Unlock()
}
resp, err := c.client.Do(req)
if err != nil {
return err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
resp.Body.Close()
isValidResponse := true
if c.isValidResponse != nil {
isValidResponse = c.isValidResponse(resp, body)
}
if c.onResp != nil {
suite.mu.Lock()
defer suite.mu.Unlock()
suite.generalEnd = time.Now()
c.onResp(&suite, &HTTPCallInfo{
StatusCode: resp.StatusCode,
IsValidResponse: isValidResponse,
})
}
return nil
}
// HTTPCallInfo represents a data set which passed as a function argument to an onResp callback.
type HTTPCallInfo struct {
// StatusCode is a response status code
StatusCode int
// IsValidResponse represents a fact of whether a response is treated as valid. You can read more about it in
// HTTPCaller annotation.
IsValidResponse bool
}

View File

@@ -0,0 +1,53 @@
package probing
import "log"
type Logger interface {
Fatalf(format string, v ...interface{})
Errorf(format string, v ...interface{})
Warnf(format string, v ...interface{})
Infof(format string, v ...interface{})
Debugf(format string, v ...interface{})
}
type StdLogger struct {
Logger *log.Logger
}
func (l StdLogger) Fatalf(format string, v ...interface{}) {
l.Logger.Printf("FATAL: "+format, v...)
}
func (l StdLogger) Errorf(format string, v ...interface{}) {
l.Logger.Printf("ERROR: "+format, v...)
}
func (l StdLogger) Warnf(format string, v ...interface{}) {
l.Logger.Printf("WARN: "+format, v...)
}
func (l StdLogger) Infof(format string, v ...interface{}) {
l.Logger.Printf("INFO: "+format, v...)
}
func (l StdLogger) Debugf(format string, v ...interface{}) {
l.Logger.Printf("DEBUG: "+format, v...)
}
type NoopLogger struct {
}
func (l NoopLogger) Fatalf(format string, v ...interface{}) {
}
func (l NoopLogger) Errorf(format string, v ...interface{}) {
}
func (l NoopLogger) Warnf(format string, v ...interface{}) {
}
func (l NoopLogger) Infof(format string, v ...interface{}) {
}
func (l NoopLogger) Debugf(format string, v ...interface{}) {
}

View File

@@ -0,0 +1,106 @@
package probing
import (
"net"
"runtime"
"time"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
)
type packetConn interface {
Close() error
ICMPRequestType() icmp.Type
ReadFrom(b []byte) (n int, ttl int, src net.Addr, err error)
SetFlagTTL() error
SetReadDeadline(t time.Time) error
WriteTo(b []byte, dst net.Addr) (int, error)
SetTTL(ttl int)
SetMark(m uint) error
SetDoNotFragment() error
SetBroadcastFlag() error
}
type icmpConn struct {
c *icmp.PacketConn
ttl int
}
func (c *icmpConn) Close() error {
return c.c.Close()
}
func (c *icmpConn) SetTTL(ttl int) {
c.ttl = ttl
}
func (c *icmpConn) SetReadDeadline(t time.Time) error {
return c.c.SetReadDeadline(t)
}
func (c *icmpConn) WriteTo(b []byte, dst net.Addr) (int, error) {
if c.c.IPv6PacketConn() != nil {
if err := c.c.IPv6PacketConn().SetHopLimit(c.ttl); err != nil {
return 0, err
}
}
if c.c.IPv4PacketConn() != nil {
if err := c.c.IPv4PacketConn().SetTTL(c.ttl); err != nil {
return 0, err
}
}
return c.c.WriteTo(b, dst)
}
type icmpv4Conn struct {
icmpConn
}
func (c *icmpv4Conn) SetFlagTTL() error {
err := c.c.IPv4PacketConn().SetControlMessage(ipv4.FlagTTL, true)
if runtime.GOOS == "windows" {
return nil
}
return err
}
func (c *icmpv4Conn) ReadFrom(b []byte) (int, int, net.Addr, error) {
ttl := -1
n, cm, src, err := c.c.IPv4PacketConn().ReadFrom(b)
if cm != nil {
ttl = cm.TTL
}
return n, ttl, src, err
}
func (c icmpv4Conn) ICMPRequestType() icmp.Type {
return ipv4.ICMPTypeEcho
}
type icmpV6Conn struct {
icmpConn
}
func (c *icmpV6Conn) SetFlagTTL() error {
err := c.c.IPv6PacketConn().SetControlMessage(ipv6.FlagHopLimit, true)
if runtime.GOOS == "windows" {
return nil
}
return err
}
func (c *icmpV6Conn) ReadFrom(b []byte) (int, int, net.Addr, error) {
ttl := -1
n, cm, src, err := c.c.IPv6PacketConn().ReadFrom(b)
if cm != nil {
ttl = cm.HopLimit
}
return n, ttl, src, err
}
func (c icmpV6Conn) ICMPRequestType() icmp.Type {
return ipv6.ICMPTypeEchoRequest
}

980
vendor/github.com/prometheus-community/pro-bing/ping.go generated vendored Normal file
View File

@@ -0,0 +1,980 @@
// Package probing is a simple but powerful ICMP echo (ping) library.
//
// Here is a very simple example that sends and receives three packets:
//
// pinger, err := probing.NewPinger("www.google.com")
// if err != nil {
// panic(err)
// }
// pinger.Count = 3
// err = pinger.Run() // blocks until finished
// if err != nil {
// panic(err)
// }
// stats := pinger.Statistics() // get send/receive/rtt stats
//
// Here is an example that emulates the traditional UNIX ping command:
//
// pinger, err := probing.NewPinger("www.google.com")
// if err != nil {
// panic(err)
// }
// // Listen for Ctrl-C.
// c := make(chan os.Signal, 1)
// signal.Notify(c, os.Interrupt)
// go func() {
// for _ = range c {
// pinger.Stop()
// }
// }()
// pinger.OnRecv = func(pkt *probing.Packet) {
// fmt.Printf("%d bytes from %s: icmp_seq=%d time=%v\n",
// pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt)
// }
// pinger.OnFinish = func(stats *probing.Statistics) {
// fmt.Printf("\n--- %s ping statistics ---\n", stats.Addr)
// fmt.Printf("%d packets transmitted, %d packets received, %v%% packet loss\n",
// stats.PacketsSent, stats.PacketsRecv, stats.PacketLoss)
// fmt.Printf("round-trip min/avg/max/stddev = %v/%v/%v/%v\n",
// stats.MinRtt, stats.AvgRtt, stats.MaxRtt, stats.StdDevRtt)
// }
// fmt.Printf("PING %s (%s):\n", pinger.Addr(), pinger.IPAddr())
// err = pinger.Run()
// if err != nil {
// panic(err)
// }
//
// It sends ICMP Echo Request packet(s) and waits for an Echo Reply in response.
// If it receives a response, it calls the OnRecv callback. When it's finished,
// it calls the OnFinish callback.
//
// For a full ping example, see "cmd/ping/ping.go".
package probing
import (
"bytes"
"context"
"errors"
"fmt"
"log"
"math"
"math/rand"
"net"
"runtime"
"sync"
"sync/atomic"
"syscall"
"time"
"github.com/google/uuid"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
"golang.org/x/sync/errgroup"
)
const (
timeSliceLength = 8
trackerLength = len(uuid.UUID{})
protocolICMP = 1
protocolIPv6ICMP = 58
networkIP = "ip"
networkIPv4 = "ip4"
networkIPv6 = "ip6"
)
var (
ipv4Proto = map[string]string{"icmp": "ip4:icmp", "udp": "udp4"}
ipv6Proto = map[string]string{"icmp": "ip6:ipv6-icmp", "udp": "udp6"}
ErrMarkNotSupported = errors.New("setting SO_MARK socket option is not supported on this platform")
ErrDFNotSupported = errors.New("setting do-not-fragment bit is not supported on this platform")
)
// New returns a new Pinger struct pointer.
func New(addr string) *Pinger {
r := rand.New(rand.NewSource(getSeed()))
firstUUID := uuid.New()
var firstSequence = map[uuid.UUID]map[int]struct{}{}
firstSequence[firstUUID] = make(map[int]struct{})
return &Pinger{
Count: -1,
Interval: time.Second,
RecordRtts: true,
Size: timeSliceLength + trackerLength,
Timeout: time.Duration(math.MaxInt64),
addr: addr,
done: make(chan interface{}),
id: r.Intn(math.MaxUint16),
trackerUUIDs: []uuid.UUID{firstUUID},
ipaddr: nil,
ipv4: false,
network: networkIP,
protocol: "udp",
awaitingSequences: firstSequence,
TTL: 64,
logger: StdLogger{Logger: log.New(log.Writer(), log.Prefix(), log.Flags())},
}
}
// NewPinger returns a new Pinger and resolves the address.
func NewPinger(addr string) (*Pinger, error) {
p := New(addr)
return p, p.Resolve()
}
// Pinger represents a packet sender/receiver.
type Pinger struct {
// Interval is the wait time between each packet send. Default is 1s.
Interval time.Duration
// Timeout specifies a timeout before ping exits, regardless of how many
// packets have been received.
Timeout time.Duration
// ResolveTimeout specifies a timeout to resolve an IP address or domain name
ResolveTimeout time.Duration
// Count tells pinger to stop after sending (and receiving) Count echo
// packets. If this option is not specified, pinger will operate until
// interrupted.
Count int
// Debug runs in debug mode
Debug bool
// Number of packets sent
PacketsSent int
// Number of packets received
PacketsRecv int
// Number of duplicate packets received
PacketsRecvDuplicates int
// Round trip time statistics
minRtt time.Duration
maxRtt time.Duration
avgRtt time.Duration
stdDevRtt time.Duration
stddevm2 time.Duration
statsMu sync.RWMutex
// If true, keep a record of rtts of all received packets.
// Set to false to avoid memory bloat for long running pings.
RecordRtts bool
// rtts is all of the Rtts
rtts []time.Duration
// OnSetup is called when Pinger has finished setting up the listening socket
OnSetup func()
// OnSend is called when Pinger sends a packet
OnSend func(*Packet)
// OnRecv is called when Pinger receives and processes a packet
OnRecv func(*Packet)
// OnFinish is called when Pinger exits
OnFinish func(*Statistics)
// OnDuplicateRecv is called when a packet is received that has already been received.
OnDuplicateRecv func(*Packet)
// OnSendError is called when an error occurs while Pinger attempts to send a packet
OnSendError func(*Packet, error)
// OnRecvError is called when an error occurs while Pinger attempts to receive a packet
OnRecvError func(error)
// Size of packet being sent
Size int
// Tracker: Used to uniquely identify packets - Deprecated
Tracker uint64
// Source is the source IP address
Source string
// Channel and mutex used to communicate when the Pinger should stop between goroutines.
done chan interface{}
lock sync.Mutex
ipaddr *net.IPAddr
addr string
// mark is a SO_MARK (fwmark) set on outgoing icmp packets
mark uint
// df when true sets the do-not-fragment bit in the outer IP or IPv6 header
df bool
// trackerUUIDs is the list of UUIDs being used for sending packets.
trackerUUIDs []uuid.UUID
ipv4 bool
id int
sequence int
// awaitingSequences are in-flight sequence numbers we keep track of to help remove duplicate receipts
awaitingSequences map[uuid.UUID]map[int]struct{}
// network is one of "ip", "ip4", or "ip6".
network string
// protocol is "icmp" or "udp".
protocol string
logger Logger
TTL int
}
type packet struct {
bytes []byte
nbytes int
ttl int
addr net.Addr
}
// Packet represents a received and processed ICMP echo packet.
type Packet struct {
// Rtt is the round-trip time it took to ping.
Rtt time.Duration
// IPAddr is the address of the host being pinged.
IPAddr *net.IPAddr
// Addr is the string address of the host being pinged.
Addr string
// NBytes is the number of bytes in the message.
Nbytes int
// Seq is the ICMP sequence number.
Seq int
// TTL is the Time To Live on the packet.
TTL int
// ID is the ICMP identifier.
ID int
}
// Statistics represent the stats of a currently running or finished
// pinger operation.
type Statistics struct {
// PacketsRecv is the number of packets received.
PacketsRecv int
// PacketsSent is the number of packets sent.
PacketsSent int
// PacketsRecvDuplicates is the number of duplicate responses there were to a sent packet.
PacketsRecvDuplicates int
// PacketLoss is the percentage of packets lost.
PacketLoss float64
// IPAddr is the address of the host being pinged.
IPAddr *net.IPAddr
// Addr is the string address of the host being pinged.
Addr string
// Rtts is all of the round-trip times sent via this pinger.
Rtts []time.Duration
// MinRtt is the minimum round-trip time sent via this pinger.
MinRtt time.Duration
// MaxRtt is the maximum round-trip time sent via this pinger.
MaxRtt time.Duration
// AvgRtt is the average round-trip time sent via this pinger.
AvgRtt time.Duration
// StdDevRtt is the standard deviation of the round-trip times sent via
// this pinger.
StdDevRtt time.Duration
}
func (p *Pinger) updateStatistics(pkt *Packet) {
p.statsMu.Lock()
defer p.statsMu.Unlock()
p.PacketsRecv++
if p.RecordRtts {
p.rtts = append(p.rtts, pkt.Rtt)
}
if p.PacketsRecv == 1 || pkt.Rtt < p.minRtt {
p.minRtt = pkt.Rtt
}
if pkt.Rtt > p.maxRtt {
p.maxRtt = pkt.Rtt
}
pktCount := time.Duration(p.PacketsRecv)
// welford's online method for stddev
// https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm
delta := pkt.Rtt - p.avgRtt
p.avgRtt += delta / pktCount
delta2 := pkt.Rtt - p.avgRtt
p.stddevm2 += delta * delta2
p.stdDevRtt = time.Duration(math.Sqrt(float64(p.stddevm2 / pktCount)))
}
// SetIPAddr sets the ip address of the target host.
func (p *Pinger) SetIPAddr(ipaddr *net.IPAddr) {
p.ipv4 = isIPv4(ipaddr.IP)
p.ipaddr = ipaddr
p.addr = ipaddr.String()
}
// IPAddr returns the ip address of the target host.
func (p *Pinger) IPAddr() *net.IPAddr {
return p.ipaddr
}
// Resolve does the DNS lookup for the Pinger address and sets IP protocol.
func (p *Pinger) Resolve() error {
if len(p.addr) == 0 {
return errors.New("addr cannot be empty")
}
var (
ipaddr *net.IPAddr
err error
)
if p.ResolveTimeout > time.Duration(0) {
var (
ctx = context.Background()
ips []net.IP
)
ctx, cancel := context.WithTimeout(ctx, p.ResolveTimeout)
defer cancel()
ips, err = net.DefaultResolver.LookupIP(ctx, p.network, p.addr)
if err != nil {
return err
}
if len(ips) == 0 {
return fmt.Errorf("lookup %s failed: no addresses found", p.addr)
}
ipaddr = &net.IPAddr{IP: ips[0]}
for _, ip := range ips {
if p.network == networkIPv6 {
if ip.To4() == nil && ip.To16() != nil {
ipaddr = &net.IPAddr{IP: ip}
break
}
continue
}
if ip.To4() != nil {
ipaddr = &net.IPAddr{IP: ip}
}
}
} else {
ipaddr, err = net.ResolveIPAddr(p.network, p.addr)
if err != nil {
return err
}
}
p.ipv4 = isIPv4(ipaddr.IP)
p.ipaddr = ipaddr
return nil
}
// SetAddr resolves and sets the ip address of the target host, addr can be a
// DNS name like "www.google.com" or IP like "127.0.0.1".
func (p *Pinger) SetAddr(addr string) error {
oldAddr := p.addr
p.addr = addr
err := p.Resolve()
if err != nil {
p.addr = oldAddr
return err
}
return nil
}
// Addr returns the string ip address of the target host.
func (p *Pinger) Addr() string {
return p.addr
}
// SetNetwork allows configuration of DNS resolution.
// * "ip" will automatically select IPv4 or IPv6.
// * "ip4" will select IPv4.
// * "ip6" will select IPv6.
func (p *Pinger) SetNetwork(n string) {
switch n {
case networkIPv4:
p.network = networkIPv4
case networkIPv6:
p.network = networkIPv6
default:
p.network = networkIP
}
}
// SetPrivileged sets the type of ping pinger will send.
// false means pinger will send an "unprivileged" UDP ping.
// true means pinger will send a "privileged" raw ICMP ping.
// NOTE: setting to true requires that it be run with super-user privileges.
func (p *Pinger) SetPrivileged(privileged bool) {
if privileged {
p.protocol = "icmp"
} else {
p.protocol = "udp"
}
}
// Privileged returns whether pinger is running in privileged mode.
func (p *Pinger) Privileged() bool {
return p.protocol == "icmp"
}
// SetLogger sets the logger to be used to log events from the pinger.
func (p *Pinger) SetLogger(logger Logger) {
p.logger = logger
}
// SetID sets the ICMP identifier.
func (p *Pinger) SetID(id int) {
p.id = id
}
// ID returns the ICMP identifier.
func (p *Pinger) ID() int {
return p.id
}
// SetMark sets a mark intended to be set on outgoing ICMP packets.
func (p *Pinger) SetMark(m uint) {
p.mark = m
}
// Mark returns the mark to be set on outgoing ICMP packets.
func (p *Pinger) Mark() uint {
return p.mark
}
// SetDoNotFragment sets the do-not-fragment bit in the outer IP header to the desired value.
func (p *Pinger) SetDoNotFragment(df bool) {
p.df = df
}
// Run runs the pinger. This is a blocking function that will exit when it's
// done. If Count or Interval are not specified, it will run continuously until
// it is interrupted.
func (p *Pinger) Run() error {
return p.RunWithContext(context.Background())
}
// RunWithContext runs the pinger with a context. This is a blocking function that will exit when it's
// done or if the context is canceled. If Count or Interval are not specified, it will run continuously until
// it is interrupted.
func (p *Pinger) RunWithContext(ctx context.Context) error {
var conn packetConn
var err error
if p.Size < timeSliceLength+trackerLength {
return fmt.Errorf("size %d is less than minimum required size %d", p.Size, timeSliceLength+trackerLength)
}
if p.ipaddr == nil {
err = p.Resolve()
}
if err != nil {
return err
}
if conn, err = p.listen(); err != nil {
return err
}
defer conn.Close()
if p.mark != 0 {
if err := conn.SetMark(p.mark); err != nil {
return fmt.Errorf("error setting mark: %v", err)
}
}
if p.df {
if err := conn.SetDoNotFragment(); err != nil {
return fmt.Errorf("error setting do-not-fragment: %v", err)
}
}
conn.SetTTL(p.TTL)
return p.run(ctx, conn)
}
func (p *Pinger) run(ctx context.Context, conn packetConn) error {
if err := conn.SetFlagTTL(); err != nil {
return err
}
defer p.finish()
recv := make(chan *packet, 5)
defer close(recv)
if p.OnSetup != nil {
p.OnSetup()
}
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
select {
case <-ctx.Done():
p.Stop()
return ctx.Err()
case <-p.done:
}
return nil
})
g.Go(func() error {
defer p.Stop()
return p.recvICMP(conn, recv)
})
g.Go(func() error {
defer p.Stop()
return p.runLoop(conn, recv)
})
return g.Wait()
}
func (p *Pinger) runLoop(
conn packetConn,
recvCh <-chan *packet,
) error {
logger := p.logger
if logger == nil {
logger = NoopLogger{}
}
timeout := time.NewTicker(p.Timeout)
interval := time.NewTicker(p.Interval)
defer func() {
interval.Stop()
timeout.Stop()
}()
if err := p.sendICMP(conn); err != nil {
return err
}
for {
select {
case <-p.done:
return nil
case <-timeout.C:
return nil
case r := <-recvCh:
err := p.processPacket(r)
if err != nil {
// FIXME: this logs as FATAL but continues
logger.Fatalf("processing received packet: %s", err)
}
case <-interval.C:
if p.Count > 0 && p.PacketsSent >= p.Count {
interval.Stop()
continue
}
err := p.sendICMP(conn)
if err != nil {
// FIXME: this logs as FATAL but continues
logger.Fatalf("sending packet: %s", err)
}
}
if p.Count > 0 && p.PacketsRecv >= p.Count {
return nil
}
}
}
func (p *Pinger) Stop() {
p.lock.Lock()
defer p.lock.Unlock()
open := true
select {
case _, open = <-p.done:
default:
}
if open {
close(p.done)
}
}
func (p *Pinger) finish() {
if p.OnFinish != nil {
p.OnFinish(p.Statistics())
}
}
// Statistics returns the statistics of the pinger. This can be run while the
// pinger is running or after it is finished. OnFinish calls this function to
// get it's finished statistics.
func (p *Pinger) Statistics() *Statistics {
p.statsMu.RLock()
defer p.statsMu.RUnlock()
sent := p.PacketsSent
var loss float64
if sent > 0 {
loss = float64(sent-p.PacketsRecv) / float64(sent) * 100
}
s := Statistics{
PacketsSent: sent,
PacketsRecv: p.PacketsRecv,
PacketsRecvDuplicates: p.PacketsRecvDuplicates,
PacketLoss: loss,
Rtts: p.rtts,
Addr: p.addr,
IPAddr: p.ipaddr,
MaxRtt: p.maxRtt,
MinRtt: p.minRtt,
AvgRtt: p.avgRtt,
StdDevRtt: p.stdDevRtt,
}
return &s
}
type expBackoff struct {
baseDelay time.Duration
maxExp int64
c int64
}
func (b *expBackoff) Get() time.Duration {
if b.c < b.maxExp {
b.c++
}
return b.baseDelay * time.Duration(rand.Int63n(1<<b.c))
}
func newExpBackoff(baseDelay time.Duration, maxExp int64) expBackoff {
return expBackoff{baseDelay: baseDelay, maxExp: maxExp}
}
func (p *Pinger) recvICMP(
conn packetConn,
recv chan<- *packet,
) error {
// Start by waiting for 50 µs and increase to a possible maximum of ~ 100 ms.
expBackoff := newExpBackoff(50*time.Microsecond, 11)
delay := expBackoff.Get()
// Workaround for https://github.com/golang/go/issues/47369
offset := 0
if p.ipv4 && !p.Privileged() && runtime.GOOS == "darwin" {
offset = 20
}
for {
select {
case <-p.done:
return nil
default:
bytes := make([]byte, p.getMessageLength()+offset)
if err := conn.SetReadDeadline(time.Now().Add(delay)); err != nil {
return err
}
n, ttl, addr, err := conn.ReadFrom(bytes)
if err != nil {
if p.OnRecvError != nil {
p.OnRecvError(err)
}
if neterr, ok := err.(*net.OpError); ok {
if neterr.Timeout() {
// Read timeout
delay = expBackoff.Get()
continue
}
}
return err
}
select {
case <-p.done:
return nil
case recv <- &packet{bytes: bytes, nbytes: n, ttl: ttl, addr: addr}:
}
}
}
}
// getPacketUUID scans the tracking slice for matches.
func (p *Pinger) getPacketUUID(pkt []byte) (*uuid.UUID, error) {
var packetUUID uuid.UUID
err := packetUUID.UnmarshalBinary(pkt[timeSliceLength : timeSliceLength+trackerLength])
if err != nil {
return nil, fmt.Errorf("error decoding tracking UUID: %w", err)
}
for _, item := range p.trackerUUIDs {
if item == packetUUID {
return &packetUUID, nil
}
}
return nil, nil
}
// getCurrentTrackerUUID grabs the latest tracker UUID.
func (p *Pinger) getCurrentTrackerUUID() uuid.UUID {
return p.trackerUUIDs[len(p.trackerUUIDs)-1]
}
func (p *Pinger) processPacket(recv *packet) error {
receivedAt := time.Now()
var proto int
if p.ipv4 {
proto = protocolICMP
// Workaround for https://github.com/golang/go/issues/47369
recv.nbytes = stripIPv4Header(recv.nbytes, recv.bytes)
} else {
proto = protocolIPv6ICMP
}
var m *icmp.Message
var err error
if m, err = icmp.ParseMessage(proto, recv.bytes); err != nil {
return fmt.Errorf("error parsing icmp message: %w", err)
}
if m.Type != ipv4.ICMPTypeEchoReply && m.Type != ipv6.ICMPTypeEchoReply {
// Not an echo reply, ignore it
return nil
}
// If initial ip is a broadcast ip, ping responses will come from machines' in the
// subnet, thus ip will differ. Below gets real ip from received package.
var realIP *net.IPAddr
switch v := recv.addr.(type) {
case *net.IPAddr: // For ICMP
realIP = v
case *net.UDPAddr:
realIP = &net.IPAddr{IP: v.IP}
default:
p.logger.Infof("received address: %s it neither an Ip address (ICMP) nor UDP address, shouldn't happen. using initial address", recv.addr)
realIP = p.ipaddr
}
inPkt := &Packet{
Nbytes: recv.nbytes,
IPAddr: realIP,
Addr: realIP.String(),
TTL: recv.ttl,
ID: p.id,
}
switch pkt := m.Body.(type) {
case *icmp.Echo:
if !p.matchID(pkt.ID) {
return nil
}
if len(pkt.Data) < timeSliceLength+trackerLength {
return fmt.Errorf("insufficient data received; got: %d %v",
len(pkt.Data), pkt.Data)
}
pktUUID, err := p.getPacketUUID(pkt.Data)
if err != nil || pktUUID == nil {
return err
}
timestamp := bytesToTime(pkt.Data[:timeSliceLength])
inPkt.Rtt = receivedAt.Sub(timestamp)
inPkt.Seq = pkt.Seq
// If we've already received this sequence, ignore it.
if _, inflight := p.awaitingSequences[*pktUUID][pkt.Seq]; !inflight {
p.PacketsRecvDuplicates++
if p.OnDuplicateRecv != nil {
p.OnDuplicateRecv(inPkt)
}
return nil
}
// remove it from the list of sequences we're waiting for so we don't get duplicates.
delete(p.awaitingSequences[*pktUUID], pkt.Seq)
p.updateStatistics(inPkt)
default:
// Very bad, not sure how this can happen
return fmt.Errorf("invalid ICMP echo reply; type: '%T', '%v'", pkt, pkt)
}
if p.OnRecv != nil {
p.OnRecv(inPkt)
}
return nil
}
func (p *Pinger) sendICMP(conn packetConn) error {
var dst net.Addr = p.ipaddr
if p.protocol == "udp" {
dst = &net.UDPAddr{IP: p.ipaddr.IP, Zone: p.ipaddr.Zone}
}
currentUUID := p.getCurrentTrackerUUID()
uuidEncoded, err := currentUUID.MarshalBinary()
if err != nil {
return fmt.Errorf("unable to marshal UUID binary: %w", err)
}
t := append(timeToBytes(time.Now()), uuidEncoded...)
if remainSize := p.Size - timeSliceLength - trackerLength; remainSize > 0 {
t = append(t, bytes.Repeat([]byte{1}, remainSize)...)
}
body := &icmp.Echo{
ID: p.id,
Seq: p.sequence,
Data: t,
}
msg := &icmp.Message{
Type: conn.ICMPRequestType(),
Code: 0,
Body: body,
}
msgBytes, err := msg.Marshal(nil)
if err != nil {
return err
}
for {
if _, err := conn.WriteTo(msgBytes, dst); err != nil {
// Try to set broadcast flag
if errors.Is(err, syscall.EACCES) && runtime.GOOS == "linux" {
if e := conn.SetBroadcastFlag(); e != nil {
p.logger.Warnf("had EACCES syscall error, check your local firewall")
}
p.logger.Infof("Pinging a broadcast address")
continue
}
if p.OnSendError != nil {
outPkt := &Packet{
Nbytes: len(msgBytes),
IPAddr: p.ipaddr,
Addr: p.addr,
Seq: p.sequence,
ID: p.id,
}
p.OnSendError(outPkt, err)
}
if neterr, ok := err.(*net.OpError); ok {
if neterr.Err == syscall.ENOBUFS {
continue
}
}
return err
}
if p.OnSend != nil {
outPkt := &Packet{
Nbytes: len(msgBytes),
IPAddr: p.ipaddr,
Addr: p.addr,
Seq: p.sequence,
ID: p.id,
}
p.OnSend(outPkt)
}
// mark this sequence as in-flight
p.awaitingSequences[currentUUID][p.sequence] = struct{}{}
p.PacketsSent++
p.sequence++
if p.sequence > 65535 {
newUUID := uuid.New()
p.trackerUUIDs = append(p.trackerUUIDs, newUUID)
p.awaitingSequences[newUUID] = make(map[int]struct{})
p.sequence = 0
}
break
}
return nil
}
func (p *Pinger) listen() (packetConn, error) {
var (
conn packetConn
err error
)
if p.ipv4 {
var c icmpv4Conn
c.c, err = icmp.ListenPacket(ipv4Proto[p.protocol], p.Source)
conn = &c
} else {
var c icmpV6Conn
c.c, err = icmp.ListenPacket(ipv6Proto[p.protocol], p.Source)
conn = &c
}
if err != nil {
p.Stop()
return nil, err
}
return conn, nil
}
func bytesToTime(b []byte) time.Time {
var nsec int64
for i := uint8(0); i < 8; i++ {
nsec += int64(b[i]) << ((7 - i) * 8)
}
return time.Unix(nsec/1000000000, nsec%1000000000)
}
func isIPv4(ip net.IP) bool {
return len(ip.To4()) == net.IPv4len
}
func timeToBytes(t time.Time) []byte {
nsec := t.UnixNano()
b := make([]byte, 8)
for i := uint8(0); i < 8; i++ {
b[i] = byte((nsec >> ((7 - i) * 8)) & 0xff)
}
return b
}
var seed = time.Now().UnixNano()
// getSeed returns a goroutine-safe unique seed
func getSeed() int64 {
return atomic.AddInt64(&seed, 1)
}
// stripIPv4Header strips IPv4 header bytes if present
// https://github.com/golang/go/commit/3b5be4522a21df8ce52a06a0c4ba005c89a8590f
func stripIPv4Header(n int, b []byte) int {
if len(b) < 20 {
return n
}
l := int(b[0]&0x0f) << 2
if 20 > l || l > len(b) {
return n
}
if b[0]>>4 != 4 {
return n
}
copy(b, b[l:])
return n - l
}

View File

@@ -0,0 +1,158 @@
//go:build linux
// +build linux
package probing
import (
"errors"
"os"
"reflect"
"syscall"
"golang.org/x/net/icmp"
)
// Returns the length of an ICMP message.
func (p *Pinger) getMessageLength() int {
return p.Size + 8
}
// Attempts to match the ID of an ICMP packet.
func (p *Pinger) matchID(ID int) bool {
// On Linux we can only match ID if we are privileged.
if p.protocol == "icmp" {
return ID == p.id
}
return true
}
// SetMark sets the SO_MARK socket option on outgoing ICMP packets.
// Setting this option requires CAP_NET_ADMIN.
func (c *icmpConn) SetMark(mark uint) error {
fd, err := getFD(c.c)
if err != nil {
return err
}
return os.NewSyscallError(
"setsockopt",
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(mark)),
)
}
// SetMark sets the SO_MARK socket option on outgoing ICMP packets.
// Setting this option requires CAP_NET_ADMIN.
func (c *icmpv4Conn) SetMark(mark uint) error {
fd, err := getFD(c.icmpConn.c)
if err != nil {
return err
}
return os.NewSyscallError(
"setsockopt",
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(mark)),
)
}
// SetMark sets the SO_MARK socket option on outgoing ICMP packets.
// Setting this option requires CAP_NET_ADMIN.
func (c *icmpV6Conn) SetMark(mark uint) error {
fd, err := getFD(c.icmpConn.c)
if err != nil {
return err
}
return os.NewSyscallError(
"setsockopt",
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(mark)),
)
}
// SetDoNotFragment sets the do-not-fragment bit in the IP header of outgoing ICMP packets.
func (c *icmpConn) SetDoNotFragment() error {
fd, err := getFD(c.c)
if err != nil {
return err
}
return os.NewSyscallError(
"setsockopt",
syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_MTU_DISCOVER, syscall.IP_PMTUDISC_DO),
)
}
// SetDoNotFragment sets the do-not-fragment bit in the IP header of outgoing ICMP packets.
func (c *icmpv4Conn) SetDoNotFragment() error {
fd, err := getFD(c.icmpConn.c)
if err != nil {
return err
}
return os.NewSyscallError(
"setsockopt",
syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_MTU_DISCOVER, syscall.IP_PMTUDISC_DO),
)
}
// SetDoNotFragment sets the do-not-fragment bit in the IPv6 header of outgoing ICMPv6 packets.
func (c *icmpV6Conn) SetDoNotFragment() error {
fd, err := getFD(c.icmpConn.c)
if err != nil {
return err
}
return os.NewSyscallError(
"setsockopt",
syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6, syscall.IPV6_MTU_DISCOVER, syscall.IP_PMTUDISC_DO),
)
}
func (c *icmpConn) SetBroadcastFlag() error {
fd, err := getFD(c.c)
if err != nil {
return err
}
return os.NewSyscallError(
"setsockopt",
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_BROADCAST, 1),
)
}
func (c *icmpv4Conn) SetBroadcastFlag() error {
fd, err := getFD(c.icmpConn.c)
if err != nil {
return err
}
return os.NewSyscallError(
"setsockopt",
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_BROADCAST, 1),
)
}
func (c *icmpV6Conn) SetBroadcastFlag() error {
fd, err := getFD(c.icmpConn.c)
if err != nil {
return err
}
return os.NewSyscallError(
"setsockopt",
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_BROADCAST, 1),
)
}
// 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
}

View File

@@ -0,0 +1,60 @@
//go:build !linux && !windows
// +build !linux,!windows
package probing
// Returns the length of an ICMP message.
func (p *Pinger) getMessageLength() int {
return p.Size + 8
}
// Attempts to match the ID of an ICMP packet.
func (p *Pinger) matchID(ID int) bool {
return ID == p.id
}
// SetMark sets the SO_MARK socket option on outgoing ICMP packets.
// Setting this option requires CAP_NET_ADMIN.
func (c *icmpConn) SetMark(mark uint) error {
return ErrMarkNotSupported
}
// SetMark sets the SO_MARK socket option on outgoing ICMP packets.
// Setting this option requires CAP_NET_ADMIN.
func (c *icmpv4Conn) SetMark(mark uint) error {
return ErrMarkNotSupported
}
// SetMark sets the SO_MARK socket option on outgoing ICMP packets.
// Setting this option requires CAP_NET_ADMIN.
func (c *icmpV6Conn) SetMark(mark uint) error {
return ErrMarkNotSupported
}
// SetDoNotFragment sets the do-not-fragment bit in the IP header of outgoing ICMP packets.
func (c *icmpConn) SetDoNotFragment() error {
return ErrDFNotSupported
}
// SetDoNotFragment sets the do-not-fragment bit in the IP header of outgoing ICMP packets.
func (c *icmpv4Conn) SetDoNotFragment() error {
return ErrDFNotSupported
}
// SetDoNotFragment sets the do-not-fragment bit in the IPv6 header of outgoing ICMPv6 packets.
func (c *icmpV6Conn) SetDoNotFragment() error {
return ErrDFNotSupported
}
// No need for SetBroadcastFlag in non-linux OSes
func (c *icmpConn) SetBroadcastFlag() error {
return nil
}
func (c *icmpv4Conn) SetBroadcastFlag() error {
return nil
}
func (c *icmpV6Conn) SetBroadcastFlag() error {
return nil
}

View File

@@ -0,0 +1,71 @@
//go:build windows
// +build windows
package probing
import (
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
)
// Returns the length of an ICMP message, plus the IP packet header.
func (p *Pinger) getMessageLength() int {
if p.ipv4 {
return p.Size + 8 + ipv4.HeaderLen
}
return p.Size + 8 + ipv6.HeaderLen
}
// Attempts to match the ID of an ICMP packet.
func (p *Pinger) matchID(ID int) bool {
if ID != p.id {
return false
}
return true
}
// SetMark sets the SO_MARK socket option on outgoing ICMP packets.
// Setting this option requires CAP_NET_ADMIN.
func (c *icmpConn) SetMark(mark uint) error {
return ErrMarkNotSupported
}
// SetMark sets the SO_MARK socket option on outgoing ICMP packets.
// Setting this option requires CAP_NET_ADMIN.
func (c *icmpv4Conn) SetMark(mark uint) error {
return ErrMarkNotSupported
}
// SetMark sets the SO_MARK socket option on outgoing ICMP packets.
// Setting this option requires CAP_NET_ADMIN.
func (c *icmpV6Conn) SetMark(mark uint) error {
return ErrMarkNotSupported
}
// SetDoNotFragment sets the do-not-fragment bit in the IP header of outgoing ICMP packets.
func (c *icmpConn) SetDoNotFragment() error {
return ErrDFNotSupported
}
// SetDoNotFragment sets the do-not-fragment bit in the IP header of outgoing ICMP packets.
func (c *icmpv4Conn) SetDoNotFragment() error {
return ErrDFNotSupported
}
// SetDoNotFragment sets the do-not-fragment bit in the IPv6 header of outgoing ICMPv6 packets.
func (c *icmpV6Conn) SetDoNotFragment() error {
return ErrDFNotSupported
}
// No need for SetBroadcastFlag in non-linux OSes
func (c *icmpConn) SetBroadcastFlag() error {
return nil
}
func (c *icmpv4Conn) SetBroadcastFlag() error {
return nil
}
func (c *icmpV6Conn) SetBroadcastFlag() error {
return nil
}