├── .gitignore ├── MAINTAINERS.md ├── .github ├── dependabot.yml └── workflows │ └── golangci-lint.yml ├── CODE_OF_CONDUCT.md ├── SECURITY.md ├── go.mod ├── .editorconfig ├── .golangci.yml ├── go.sum ├── Makefile ├── .goreleaser.yaml ├── LICENSE ├── utils_windows_test.go ├── logger.go ├── .circleci └── config.yml ├── utils_other.go ├── CONTRIBUTING.md ├── utils_windows.go ├── cmd └── ping │ └── ping.go ├── packetconn.go ├── utils_linux.go ├── README.md ├── http_test.go ├── http.go ├── ping_test.go └── ping.go /.gitignore: -------------------------------------------------------------------------------- 1 | /ping 2 | /dist 3 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Maintainers 2 | 3 | * Ben Kochie @SuperQ 4 | * Matthias Loibl @metalmatze 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Prometheus Community Code of Conduct 2 | 3 | Prometheus follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). 4 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting a security issue 2 | 3 | The Prometheus security policy, including how to report vulnerabilities, can be 4 | found here: 5 | 6 | 7 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/prometheus-community/pro-bing 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/google/uuid v1.6.0 7 | golang.org/x/net v0.44.0 8 | golang.org/x/sync v0.17.0 9 | ) 10 | 11 | require golang.org/x/sys v0.36.0 // indirect 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | charset = utf-8 10 | indent_style = space 11 | 12 | [Makefile] 13 | indent_style = tab 14 | 15 | [*.go] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - misspell 5 | - revive 6 | settings: 7 | revive: 8 | rules: 9 | - name: unused-parameter 10 | severity: warning 11 | disabled: true 12 | exclusions: 13 | generated: lax 14 | presets: 15 | - comments 16 | - common-false-positives 17 | - legacy 18 | - std-error-handling 19 | rules: 20 | - linters: 21 | - errcheck 22 | path: _test.go 23 | formatters: 24 | exclusions: 25 | generated: lax 26 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 2 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 3 | golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= 4 | golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= 5 | golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= 6 | golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 7 | golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= 8 | golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO ?= go 2 | GOFMT ?= $(GO)fmt 3 | GOOPTS ?= 4 | GO111MODULE := 5 | pkgs = ./... 6 | 7 | all: style vet build test 8 | 9 | .PHONY: build 10 | build: 11 | @echo ">> building ping" 12 | GO111MODULE=$(GO111MODULE) $(GO) build $(GOOPTS) ./cmd/ping 13 | 14 | .PHONY: style 15 | style: 16 | @echo ">> checking code style" 17 | @fmtRes=$$($(GOFMT) -d $$(find . -path ./vendor -prune -o -name '*.go' -print)); \ 18 | if [ -n "$${fmtRes}" ]; then \ 19 | echo "gofmt checking failed!"; echo "$${fmtRes}"; echo; \ 20 | echo "Please ensure you are using $$($(GO) version) for formatting code."; \ 21 | exit 1; \ 22 | fi 23 | 24 | .PHONY: test 25 | test: 26 | @echo ">> running all tests" 27 | GO111MODULE=$(GO111MODULE) $(GO) test -race -cover $(GOOPTS) $(pkgs) 28 | 29 | .PHONY: vet 30 | vet: 31 | @echo ">> vetting code" 32 | GO111MODULE=$(GO111MODULE) $(GO) vet $(GOOPTS) $(pkgs) 33 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: ping 2 | before: 3 | hooks: 4 | - go mod download 5 | builds: 6 | - binary: ping 7 | dir: cmd/ping 8 | goarch: 9 | - amd64 10 | - arm 11 | - arm64 12 | goarm: 13 | - 6 14 | - 7 15 | goos: 16 | - darwin 17 | - freebsd 18 | - linux 19 | - windows 20 | archives: 21 | - files: 22 | - LICENSE 23 | - README.md 24 | format_overrides: 25 | - goos: windows 26 | format: zip 27 | wrap_in_directory: true 28 | # TODO: Decide if we want packages (name conflcits with /bin/ping?) 29 | # nfpms: 30 | # homepage: https://github.com/go-ping/ping 31 | # maintainer: 'Go Ping Maintainers ' 32 | # description: Ping written in Go. 33 | # license: MIT 34 | # formats: 35 | # - deb 36 | # - rpm 37 | checksum: 38 | name_template: 'checksums.txt' 39 | snapshot: 40 | name_template: "{{ .Tag }}-{{ .ShortCommit }}" 41 | changelog: 42 | sort: asc 43 | filters: 44 | exclude: 45 | - '^docs:' 46 | - '^test:' 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2022 The Prometheus Authors 4 | Copyright 2016 Cameron Sparr and contributors. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /utils_windows_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package probing 5 | 6 | import "testing" 7 | 8 | func TestGetMessageLength(t *testing.T) { 9 | tests := []struct { 10 | description string 11 | pinger *Pinger 12 | expected int 13 | }{ 14 | { 15 | description: "IPv4 total size < 2048", 16 | pinger: &Pinger{ 17 | Size: 24, // default size 18 | ipv4: true, 19 | }, 20 | expected: 2048, 21 | }, 22 | { 23 | description: "IPv4 total size > 2048", 24 | pinger: &Pinger{ 25 | Size: 1993, // 2048 - 2 * (ipv4.HeaderLen + 8) + 1 26 | ipv4: true, 27 | }, 28 | expected: 2049, 29 | }, 30 | { 31 | description: "IPv6 total size < 2048", 32 | pinger: &Pinger{ 33 | Size: 24, 34 | ipv4: false, 35 | }, 36 | expected: 2048, 37 | }, 38 | { 39 | description: "IPv6 total size > 2048", 40 | pinger: &Pinger{ 41 | Size: 1953, // 2048 - 2 * (ipv6.HeaderLen + 8) + 1 42 | ipv4: false, 43 | }, 44 | expected: 2049, 45 | }, 46 | } 47 | 48 | for _, tt := range tests { 49 | t.Run(tt.description, func(t *testing.T) { 50 | actual := tt.pinger.getMessageLength() 51 | if tt.expected != actual { 52 | t.Fatalf("unexpected message length, expected: %d, actual %d", tt.expected, actual) 53 | } 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package probing 2 | 3 | import "log" 4 | 5 | type Logger interface { 6 | Fatalf(format string, v ...interface{}) 7 | Errorf(format string, v ...interface{}) 8 | Warnf(format string, v ...interface{}) 9 | Infof(format string, v ...interface{}) 10 | Debugf(format string, v ...interface{}) 11 | } 12 | 13 | type StdLogger struct { 14 | Logger *log.Logger 15 | } 16 | 17 | func (l StdLogger) Fatalf(format string, v ...interface{}) { 18 | l.Logger.Printf("FATAL: "+format, v...) 19 | } 20 | 21 | func (l StdLogger) Errorf(format string, v ...interface{}) { 22 | l.Logger.Printf("ERROR: "+format, v...) 23 | } 24 | 25 | func (l StdLogger) Warnf(format string, v ...interface{}) { 26 | l.Logger.Printf("WARN: "+format, v...) 27 | } 28 | 29 | func (l StdLogger) Infof(format string, v ...interface{}) { 30 | l.Logger.Printf("INFO: "+format, v...) 31 | } 32 | 33 | func (l StdLogger) Debugf(format string, v ...interface{}) { 34 | l.Logger.Printf("DEBUG: "+format, v...) 35 | } 36 | 37 | type NoopLogger struct { 38 | } 39 | 40 | func (l NoopLogger) Fatalf(format string, v ...interface{}) { 41 | } 42 | 43 | func (l NoopLogger) Errorf(format string, v ...interface{}) { 44 | } 45 | 46 | func (l NoopLogger) Warnf(format string, v ...interface{}) { 47 | } 48 | 49 | func (l NoopLogger) Infof(format string, v ...interface{}) { 50 | } 51 | 52 | func (l NoopLogger) Debugf(format string, v ...interface{}) { 53 | } 54 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2.1 3 | 4 | orbs: 5 | go: circleci/go@1.7.1 6 | goreleaser: hubci/goreleaser@2.5.0 7 | 8 | jobs: 9 | build: 10 | parameters: 11 | go_version: 12 | type: string 13 | use_gomod_cache: 14 | type: boolean 15 | default: true 16 | machine: 17 | image: ubuntu-2204:current 18 | steps: 19 | - checkout 20 | - when: 21 | condition: << parameters.use_gomod_cache >> 22 | steps: 23 | - go/load-cache: 24 | key: v1-go<< parameters.go_version >> 25 | - run: go mod download 26 | - run: make 27 | - when: 28 | condition: << parameters.use_gomod_cache >> 29 | steps: 30 | - go/save-cache: 31 | key: v1-go<< parameters.go_version >> 32 | 33 | workflows: 34 | version: 2 35 | pro-bing: 36 | jobs: 37 | - build: 38 | name: go-<< matrix.go_version >> 39 | matrix: 40 | parameters: 41 | go_version: 42 | - "1.24" 43 | - "1.25" 44 | filters: 45 | tags: 46 | only: /.*/ 47 | - goreleaser/release: 48 | name: test-release 49 | version: '2.12.6' 50 | go-version: '1.25.3' 51 | dry-run: true 52 | requires: 53 | - build 54 | filters: 55 | tags: 56 | only: /.*/ 57 | - goreleaser/release: 58 | name: release 59 | version: '2.12.6' 60 | go-version: '1.25.3' 61 | requires: 62 | - build 63 | filters: 64 | tags: 65 | only: /^v.*/ 66 | branches: 67 | ignore: /.*/ 68 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This action is synced from https://github.com/prometheus/prometheus 3 | name: golangci-lint 4 | on: 5 | push: 6 | paths: 7 | - "go.sum" 8 | - "go.mod" 9 | - "**.go" 10 | - "scripts/errcheck_excludes.txt" 11 | - ".github/workflows/golangci-lint.yml" 12 | - ".golangci.yml" 13 | pull_request: 14 | 15 | permissions: # added using https://github.com/step-security/secure-repo 16 | contents: read 17 | 18 | jobs: 19 | golangci: 20 | permissions: 21 | contents: read # for actions/checkout to fetch code 22 | pull-requests: read # for golangci/golangci-lint-action to fetch pull requests 23 | name: lint 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 28 | with: 29 | persist-credentials: false 30 | - name: Install Go 31 | uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 32 | with: 33 | go-version: 1.25.x 34 | - name: Install snmp_exporter/generator dependencies 35 | run: sudo apt-get update && sudo apt-get -y install libsnmp-dev 36 | if: github.repository == 'prometheus/snmp_exporter' 37 | - name: Get golangci-lint version 38 | id: golangci-lint-version 39 | run: echo "version=$(make print-golangci-lint-version)" >> $GITHUB_OUTPUT 40 | - name: Lint 41 | uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 42 | with: 43 | args: --verbose 44 | version: ${{ steps.golangci-lint-version.outputs.version }} 45 | -------------------------------------------------------------------------------- /utils_other.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !windows 2 | // +build !linux,!windows 3 | 4 | package probing 5 | 6 | // Returns the length of an ICMP message. 7 | func (p *Pinger) getMessageLength() int { 8 | return p.Size + 8 9 | } 10 | 11 | // Attempts to match the ID of an ICMP packet. 12 | func (p *Pinger) matchID(ID int) bool { 13 | return ID == p.id 14 | } 15 | 16 | // SetMark sets the SO_MARK socket option on outgoing ICMP packets. 17 | // Setting this option requires CAP_NET_ADMIN. 18 | func (c *icmpConn) SetMark(mark uint) error { 19 | return ErrMarkNotSupported 20 | } 21 | 22 | // SetMark sets the SO_MARK socket option on outgoing ICMP packets. 23 | // Setting this option requires CAP_NET_ADMIN. 24 | func (c *icmpv4Conn) SetMark(mark uint) error { 25 | return ErrMarkNotSupported 26 | } 27 | 28 | // SetMark sets the SO_MARK socket option on outgoing ICMP packets. 29 | // Setting this option requires CAP_NET_ADMIN. 30 | func (c *icmpV6Conn) SetMark(mark uint) error { 31 | return ErrMarkNotSupported 32 | } 33 | 34 | // SetDoNotFragment sets the do-not-fragment bit in the IP header of outgoing ICMP packets. 35 | func (c *icmpConn) SetDoNotFragment() error { 36 | return ErrDFNotSupported 37 | } 38 | 39 | // SetDoNotFragment sets the do-not-fragment bit in the IP header of outgoing ICMP packets. 40 | func (c *icmpv4Conn) SetDoNotFragment() error { 41 | return ErrDFNotSupported 42 | } 43 | 44 | // SetDoNotFragment sets the do-not-fragment bit in the IPv6 header of outgoing ICMPv6 packets. 45 | func (c *icmpV6Conn) SetDoNotFragment() error { 46 | return ErrDFNotSupported 47 | } 48 | 49 | // No need for SetBroadcastFlag in non-linux OSes 50 | func (c *icmpConn) SetBroadcastFlag() error { 51 | return nil 52 | } 53 | 54 | func (c *icmpv4Conn) SetBroadcastFlag() error { 55 | return nil 56 | } 57 | 58 | func (c *icmpV6Conn) SetBroadcastFlag() error { 59 | return nil 60 | } 61 | 62 | func (c *icmpv4Conn) InstallICMPIDFilter(id int) error { 63 | return nil 64 | } 65 | 66 | func (c *icmpV6Conn) InstallICMPIDFilter(id int) error { 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First off, thanks for taking the time to contribute! 4 | 5 | Remember that this is open source software so please consider the other people who will read your code. 6 | Make it look nice for them, document your logic in comments and add or update the unit test cases. 7 | 8 | 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. 9 | 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/). 10 | 11 | ## Pull Requests 12 | 13 | [Fork the repo on GitHub](https://github.com/prometheus-community/pro-bing/fork) and clone it to your local machine. 14 | 15 | ```bash 16 | git clone https://github.com/YOUR_USERNAME/pro-bing.git && cd pro-bing 17 | ``` 18 | 19 | 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). 20 | 21 | Check out a new branch, make changes, run tests, commit & sign-off, then push branch to your fork. 22 | 23 | ```bash 24 | $ git checkout -b 25 | # edit files 26 | $ make style vet test 27 | $ git add 28 | $ git commit -s 29 | $ git push 30 | ``` 31 | 32 | Open a [new pull request](https://github.com/prometheus-community/pro-bing/compare) in the main `prometheus-community/pro-bing` repository. 33 | Please describe the purpose of your PR and remember link it to any related issues. 34 | 35 | *We may ask you to rebase your feature branch or squash the commits in order to keep the history clean.* 36 | 37 | ## Development Guides 38 | 39 | - Run `make style vet test` before committing your changes. 40 | - Document your logic in code comments. 41 | - Add tests for bug fixes and new features. 42 | - Use UNIX-style (LF) line endings. 43 | - End every file with a single blank line. 44 | - Use the UTF-8 character set. 45 | -------------------------------------------------------------------------------- /utils_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package probing 5 | 6 | import ( 7 | "math" 8 | 9 | "golang.org/x/net/ipv4" 10 | "golang.org/x/net/ipv6" 11 | ) 12 | 13 | const ( 14 | minimumBufferLength = 2048 15 | ) 16 | 17 | // Returns the length of an ICMP message, plus the IP packet header. 18 | // Calculated as: 19 | // len(ICMP request data) + 2 * (len(ICMP header) + len(IP header)) 20 | // 21 | // On Windows, the buffer needs to be able to contain: 22 | // - Response IP Header 23 | // - Response ICMP Header 24 | // - Request IP Header 25 | // - Request ICMP Header 26 | // - Request Data 27 | func (p *Pinger) getMessageLength() int { 28 | if p.ipv4 { 29 | calculatedLength := p.Size + (ipv4.HeaderLen+8)*2 30 | return int(math.Max(float64(calculatedLength), float64(minimumBufferLength))) 31 | } 32 | calculatedLength := p.Size + (ipv6.HeaderLen+8)*2 33 | return int(math.Max(float64(calculatedLength), float64(minimumBufferLength))) 34 | } 35 | 36 | // Attempts to match the ID of an ICMP packet. 37 | func (p *Pinger) matchID(ID int) bool { 38 | if ID != p.id { 39 | return false 40 | } 41 | return true 42 | } 43 | 44 | // SetMark sets the SO_MARK socket option on outgoing ICMP packets. 45 | // Setting this option requires CAP_NET_ADMIN. 46 | func (c *icmpConn) SetMark(mark uint) error { 47 | return ErrMarkNotSupported 48 | } 49 | 50 | // SetMark sets the SO_MARK socket option on outgoing ICMP packets. 51 | // Setting this option requires CAP_NET_ADMIN. 52 | func (c *icmpv4Conn) SetMark(mark uint) error { 53 | return ErrMarkNotSupported 54 | } 55 | 56 | // SetMark sets the SO_MARK socket option on outgoing ICMP packets. 57 | // Setting this option requires CAP_NET_ADMIN. 58 | func (c *icmpV6Conn) SetMark(mark uint) error { 59 | return ErrMarkNotSupported 60 | } 61 | 62 | // SetDoNotFragment sets the do-not-fragment bit in the IP header of outgoing ICMP packets. 63 | func (c *icmpConn) SetDoNotFragment() error { 64 | return ErrDFNotSupported 65 | } 66 | 67 | // SetDoNotFragment sets the do-not-fragment bit in the IP header of outgoing ICMP packets. 68 | func (c *icmpv4Conn) SetDoNotFragment() error { 69 | return ErrDFNotSupported 70 | } 71 | 72 | // SetDoNotFragment sets the do-not-fragment bit in the IPv6 header of outgoing ICMPv6 packets. 73 | func (c *icmpV6Conn) SetDoNotFragment() error { 74 | return ErrDFNotSupported 75 | } 76 | 77 | // No need for SetBroadcastFlag in non-linux OSes 78 | func (c *icmpConn) SetBroadcastFlag() error { 79 | return nil 80 | } 81 | 82 | func (c *icmpv4Conn) SetBroadcastFlag() error { 83 | return nil 84 | } 85 | 86 | func (c *icmpV6Conn) SetBroadcastFlag() error { 87 | return nil 88 | } 89 | 90 | func (c *icmpv4Conn) InstallICMPIDFilter(id int) error { 91 | return nil 92 | } 93 | 94 | func (c *icmpV6Conn) InstallICMPIDFilter(id int) error { 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /cmd/ping/ping.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "time" 9 | 10 | probing "github.com/prometheus-community/pro-bing" 11 | ) 12 | 13 | var examples = ` 14 | Examples: 15 | 16 | # ping google continuously 17 | ping www.google.com 18 | 19 | # ping google 5 times 20 | ping -c 5 www.google.com 21 | 22 | # ping google 5 times at 500ms intervals 23 | ping -c 5 -i 500ms www.google.com 24 | 25 | # ping google for 10 seconds 26 | ping -t 10s www.google.com 27 | 28 | # ping google specified interface 29 | ping -I eth1 www.goole.com 30 | 31 | # Send a privileged raw ICMP ping 32 | sudo ping --privileged www.google.com 33 | 34 | # Send ICMP messages with a 100-byte payload 35 | ping -s 100 1.1.1.1 36 | 37 | # Send ICMP messages with DSCP CS4 and ECN bits set to 0 38 | ping -Q 128 8.8.8.8 39 | ` 40 | 41 | func main() { 42 | timeout := flag.Duration("t", time.Second*100000, "") 43 | interval := flag.Duration("i", time.Second, "") 44 | count := flag.Int("c", -1, "") 45 | size := flag.Int("s", 24, "") 46 | ttl := flag.Int("l", 64, "TTL") 47 | iface := flag.String("I", "", "interface name") 48 | tclass := flag.Int("Q", 192, "Set Quality of Service related bits in ICMP datagrams (DSCP + ECN bits). Only decimal number supported") 49 | privileged := flag.Bool("privileged", false, "") 50 | flag.Usage = func() { 51 | out := flag.CommandLine.Output() 52 | fmt.Fprintf(out, "Usage of %s:\n", os.Args[0]) 53 | flag.PrintDefaults() 54 | fmt.Fprint(out, examples) 55 | } 56 | flag.Parse() 57 | 58 | if flag.NArg() == 0 { 59 | flag.Usage() 60 | return 61 | } 62 | 63 | host := flag.Arg(0) 64 | pinger, err := probing.NewPinger(host) 65 | if err != nil { 66 | fmt.Println("ERROR:", err) 67 | return 68 | } 69 | 70 | // listen for ctrl-C signal 71 | c := make(chan os.Signal, 1) 72 | signal.Notify(c, os.Interrupt) 73 | go func() { 74 | for range c { 75 | pinger.Stop() 76 | } 77 | }() 78 | 79 | pinger.OnRecv = func(pkt *probing.Packet) { 80 | fmt.Printf("%d bytes from %s: icmp_seq=%d time=%v ttl=%v\n", 81 | pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt, pkt.TTL) 82 | } 83 | pinger.OnDuplicateRecv = func(pkt *probing.Packet) { 84 | fmt.Printf("%d bytes from %s: icmp_seq=%d time=%v ttl=%v (DUP!)\n", 85 | pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt, pkt.TTL) 86 | } 87 | pinger.OnFinish = func(stats *probing.Statistics) { 88 | fmt.Printf("\n--- %s ping statistics ---\n", stats.Addr) 89 | fmt.Printf("%d packets transmitted, %d packets received, %d duplicates, %v%% packet loss\n", 90 | stats.PacketsSent, stats.PacketsRecv, stats.PacketsRecvDuplicates, stats.PacketLoss) 91 | fmt.Printf("round-trip min/avg/max/stddev = %v/%v/%v/%v\n", 92 | stats.MinRtt, stats.AvgRtt, stats.MaxRtt, stats.StdDevRtt) 93 | } 94 | 95 | pinger.Count = *count 96 | pinger.Size = *size 97 | pinger.Interval = *interval 98 | pinger.Timeout = *timeout 99 | pinger.TTL = *ttl 100 | pinger.InterfaceName = *iface 101 | pinger.SetPrivileged(*privileged) 102 | pinger.SetTrafficClass(uint8(*tclass)) 103 | 104 | fmt.Printf("PING %s (%s):\n", pinger.Addr(), pinger.IPAddr()) 105 | err = pinger.Run() 106 | if err != nil { 107 | fmt.Println("Failed to ping target host:", err) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /packetconn.go: -------------------------------------------------------------------------------- 1 | package probing 2 | 3 | import ( 4 | "net" 5 | "runtime" 6 | "time" 7 | 8 | "golang.org/x/net/icmp" 9 | "golang.org/x/net/ipv4" 10 | "golang.org/x/net/ipv6" 11 | ) 12 | 13 | type packetConn interface { 14 | Close() error 15 | ICMPRequestType() icmp.Type 16 | ReadFrom(b []byte) (n int, ttl int, src net.Addr, err error) 17 | SetFlagTTL() error 18 | SetReadDeadline(t time.Time) error 19 | WriteTo(b []byte, dst net.Addr) (int, error) 20 | SetTTL(ttl int) 21 | SetMark(m uint) error 22 | SetDoNotFragment() error 23 | SetBroadcastFlag() error 24 | SetIfIndex(ifIndex int) 25 | SetSource(source net.IP) 26 | SetTrafficClass(uint8) error 27 | InstallICMPIDFilter(id int) error 28 | } 29 | 30 | type icmpConn struct { 31 | c *icmp.PacketConn 32 | ttl int 33 | ifIndex int 34 | source net.IP 35 | } 36 | 37 | func (c *icmpConn) Close() error { 38 | return c.c.Close() 39 | } 40 | 41 | func (c *icmpConn) SetTTL(ttl int) { 42 | c.ttl = ttl 43 | } 44 | 45 | func (c *icmpConn) SetIfIndex(ifIndex int) { 46 | c.ifIndex = ifIndex 47 | } 48 | 49 | func (c *icmpConn) SetSource(source net.IP) { 50 | c.source = source 51 | } 52 | 53 | func (c *icmpConn) SetReadDeadline(t time.Time) error { 54 | return c.c.SetReadDeadline(t) 55 | } 56 | 57 | type icmpv4Conn struct { 58 | icmpConn 59 | } 60 | 61 | func (c *icmpv4Conn) SetFlagTTL() error { 62 | err := c.c.IPv4PacketConn().SetControlMessage(ipv4.FlagTTL, true) 63 | if runtime.GOOS == "windows" { 64 | return nil 65 | } 66 | return err 67 | } 68 | 69 | func (c *icmpv4Conn) SetTrafficClass(tclass uint8) error { 70 | return c.c.IPv4PacketConn().SetTOS(int(tclass)) 71 | } 72 | 73 | func (c *icmpv4Conn) ReadFrom(b []byte) (int, int, net.Addr, error) { 74 | ttl := -1 75 | n, cm, src, err := c.c.IPv4PacketConn().ReadFrom(b) 76 | if cm != nil { 77 | ttl = cm.TTL 78 | } 79 | return n, ttl, src, err 80 | } 81 | 82 | func (c *icmpv4Conn) WriteTo(b []byte, dst net.Addr) (int, error) { 83 | if err := c.c.IPv4PacketConn().SetTTL(c.ttl); err != nil { 84 | return 0, err 85 | } 86 | var cm *ipv4.ControlMessage 87 | if 1 <= c.ifIndex { 88 | // c.ifIndex == 0 if not set interface 89 | if err := c.c.IPv4PacketConn().SetControlMessage(ipv4.FlagInterface, true); err != nil { 90 | return 0, err 91 | } 92 | cm = &ipv4.ControlMessage{IfIndex: c.ifIndex} 93 | } 94 | 95 | if c.source != nil { 96 | if cm == nil { 97 | cm = &ipv4.ControlMessage{} 98 | } 99 | cm.Src = c.source 100 | } 101 | 102 | return c.c.IPv4PacketConn().WriteTo(b, cm, dst) 103 | } 104 | 105 | func (c icmpv4Conn) ICMPRequestType() icmp.Type { 106 | return ipv4.ICMPTypeEcho 107 | } 108 | 109 | type icmpV6Conn struct { 110 | icmpConn 111 | } 112 | 113 | func (c *icmpV6Conn) SetFlagTTL() error { 114 | err := c.c.IPv6PacketConn().SetControlMessage(ipv6.FlagHopLimit, true) 115 | if runtime.GOOS == "windows" { 116 | return nil 117 | } 118 | return err 119 | } 120 | 121 | func (c *icmpV6Conn) SetTrafficClass(tclass uint8) error { 122 | return c.c.IPv6PacketConn().SetTrafficClass(int(tclass)) 123 | } 124 | 125 | func (c *icmpV6Conn) ReadFrom(b []byte) (int, int, net.Addr, error) { 126 | ttl := -1 127 | n, cm, src, err := c.c.IPv6PacketConn().ReadFrom(b) 128 | if cm != nil { 129 | ttl = cm.HopLimit 130 | } 131 | return n, ttl, src, err 132 | } 133 | 134 | func (c *icmpV6Conn) WriteTo(b []byte, dst net.Addr) (int, error) { 135 | if err := c.c.IPv6PacketConn().SetHopLimit(c.ttl); err != nil { 136 | return 0, err 137 | } 138 | var cm *ipv6.ControlMessage 139 | if 1 <= c.ifIndex { 140 | // c.ifIndex == 0 if not set interface 141 | if err := c.c.IPv6PacketConn().SetControlMessage(ipv6.FlagInterface, true); err != nil { 142 | return 0, err 143 | } 144 | cm = &ipv6.ControlMessage{IfIndex: c.ifIndex} 145 | } 146 | 147 | if c.source != nil { 148 | if cm == nil { 149 | cm = &ipv6.ControlMessage{} 150 | } 151 | cm.Src = c.source 152 | } 153 | 154 | return c.c.IPv6PacketConn().WriteTo(b, cm, dst) 155 | } 156 | 157 | func (c icmpV6Conn) ICMPRequestType() icmp.Type { 158 | return ipv6.ICMPTypeEchoRequest 159 | } 160 | -------------------------------------------------------------------------------- /utils_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package probing 5 | 6 | import ( 7 | "errors" 8 | "os" 9 | "reflect" 10 | "syscall" 11 | 12 | "golang.org/x/net/bpf" 13 | "golang.org/x/net/icmp" 14 | "golang.org/x/net/ipv4" 15 | "golang.org/x/net/ipv6" 16 | ) 17 | 18 | // Returns the length of an ICMP message. 19 | func (p *Pinger) getMessageLength() int { 20 | return p.Size + 8 21 | } 22 | 23 | // Attempts to match the ID of an ICMP packet. 24 | func (p *Pinger) matchID(ID int) bool { 25 | // On Linux we can only match ID if we are privileged. 26 | if p.protocol == "icmp" { 27 | return ID == p.id 28 | } 29 | return true 30 | } 31 | 32 | // SetMark sets the SO_MARK socket option on outgoing ICMP packets. 33 | // Setting this option requires CAP_NET_ADMIN. 34 | func (c *icmpConn) SetMark(mark uint) error { 35 | fd, err := getFD(c.c) 36 | if err != nil { 37 | return err 38 | } 39 | return os.NewSyscallError( 40 | "setsockopt", 41 | syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(mark)), 42 | ) 43 | } 44 | 45 | // SetMark sets the SO_MARK socket option on outgoing ICMP packets. 46 | // Setting this option requires CAP_NET_ADMIN. 47 | func (c *icmpv4Conn) SetMark(mark uint) error { 48 | fd, err := getFD(c.c) 49 | if err != nil { 50 | return err 51 | } 52 | return os.NewSyscallError( 53 | "setsockopt", 54 | syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(mark)), 55 | ) 56 | } 57 | 58 | // SetMark sets the SO_MARK socket option on outgoing ICMP packets. 59 | // Setting this option requires CAP_NET_ADMIN. 60 | func (c *icmpV6Conn) SetMark(mark uint) error { 61 | fd, err := getFD(c.c) 62 | if err != nil { 63 | return err 64 | } 65 | return os.NewSyscallError( 66 | "setsockopt", 67 | syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(mark)), 68 | ) 69 | } 70 | 71 | // SetDoNotFragment sets the do-not-fragment bit in the IP header of outgoing ICMP packets. 72 | func (c *icmpConn) SetDoNotFragment() error { 73 | fd, err := getFD(c.c) 74 | if err != nil { 75 | return err 76 | } 77 | return os.NewSyscallError( 78 | "setsockopt", 79 | syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_MTU_DISCOVER, syscall.IP_PMTUDISC_DO), 80 | ) 81 | } 82 | 83 | // SetDoNotFragment sets the do-not-fragment bit in the IP header of outgoing ICMP packets. 84 | func (c *icmpv4Conn) SetDoNotFragment() error { 85 | fd, err := getFD(c.c) 86 | if err != nil { 87 | return err 88 | } 89 | return os.NewSyscallError( 90 | "setsockopt", 91 | syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_MTU_DISCOVER, syscall.IP_PMTUDISC_DO), 92 | ) 93 | } 94 | 95 | // SetDoNotFragment sets the do-not-fragment bit in the IPv6 header of outgoing ICMPv6 packets. 96 | func (c *icmpV6Conn) SetDoNotFragment() error { 97 | fd, err := getFD(c.c) 98 | if err != nil { 99 | return err 100 | } 101 | return os.NewSyscallError( 102 | "setsockopt", 103 | syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6, syscall.IPV6_MTU_DISCOVER, syscall.IP_PMTUDISC_DO), 104 | ) 105 | } 106 | 107 | func (c *icmpConn) SetBroadcastFlag() error { 108 | fd, err := getFD(c.c) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | return os.NewSyscallError( 114 | "setsockopt", 115 | syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_BROADCAST, 1), 116 | ) 117 | } 118 | 119 | func (c *icmpv4Conn) SetBroadcastFlag() error { 120 | fd, err := getFD(c.c) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | return os.NewSyscallError( 126 | "setsockopt", 127 | syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_BROADCAST, 1), 128 | ) 129 | } 130 | 131 | func (c *icmpV6Conn) SetBroadcastFlag() error { 132 | fd, err := getFD(c.c) 133 | if err != nil { 134 | return err 135 | } 136 | 137 | return os.NewSyscallError( 138 | "setsockopt", 139 | syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_BROADCAST, 1), 140 | ) 141 | } 142 | 143 | // InstallICMPIDFilter attaches a BPF program to the connection to filter ICMP packets id. 144 | func (c *icmpv4Conn) InstallICMPIDFilter(id int) error { 145 | filter, err := bpf.Assemble([]bpf.Instruction{ 146 | bpf.LoadMemShift{Off: 0}, // Skip IP header 147 | bpf.LoadIndirect{Off: 4, Size: 2}, // Load ICMP echo ident 148 | bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(id), SkipTrue: 0, SkipFalse: 1}, // Jump on ICMP Echo Request (ID check) 149 | bpf.RetConstant{Val: ^uint32(0)}, // If our ID, accept the packet 150 | bpf.LoadIndirect{Off: 0, Size: 1}, // Load ICMP type 151 | bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(ipv4.ICMPTypeEchoReply), SkipTrue: 1, SkipFalse: 0}, // Check if ICMP Echo Reply 152 | bpf.RetConstant{Val: 0xFFFFFFF}, // Accept packet if it's not Echo Reply 153 | bpf.RetConstant{Val: 0}, // Reject Echo packet with wrong identifier 154 | }) 155 | if err != nil { 156 | return err 157 | } 158 | return c.c.IPv4PacketConn().SetBPF(filter) 159 | } 160 | 161 | // InstallICMPIDFilter attaches a BPF program to the connection to filter ICMPv6 packets id. 162 | func (c *icmpV6Conn) InstallICMPIDFilter(id int) error { 163 | filter, err := bpf.Assemble([]bpf.Instruction{ 164 | bpf.LoadAbsolute{Off: 4, Size: 2}, // Load ICMP echo identifier 165 | bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(id), SkipTrue: 0, SkipFalse: 1}, // Check if it matches our identifier 166 | bpf.RetConstant{Val: ^uint32(0)}, // Accept if true 167 | bpf.LoadAbsolute{Off: 0, Size: 1}, // Load ICMP type 168 | bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(ipv6.ICMPTypeEchoReply), SkipTrue: 1, SkipFalse: 0}, // Check if it is an ICMP6 echo reply 169 | bpf.RetConstant{Val: ^uint32(0)}, // Accept if false 170 | bpf.RetConstant{Val: 0}, // Reject if echo with wrong identifier 171 | }) 172 | if err != nil { 173 | return err 174 | } 175 | return c.c.IPv6PacketConn().SetBPF(filter) 176 | } 177 | 178 | // getFD gets the system file descriptor for an icmp.PacketConn 179 | func getFD(c *icmp.PacketConn) (uintptr, error) { 180 | v := reflect.ValueOf(c).Elem().FieldByName("c").Elem() 181 | if v.Elem().Kind() != reflect.Struct { 182 | return 0, errors.New("invalid type") 183 | } 184 | 185 | fd := v.Elem().FieldByName("conn").FieldByName("fd") 186 | if fd.Elem().Kind() != reflect.Struct { 187 | return 0, errors.New("invalid type") 188 | } 189 | 190 | pfd := fd.Elem().FieldByName("pfd") 191 | if pfd.Kind() != reflect.Struct { 192 | return 0, errors.New("invalid type") 193 | } 194 | 195 | return uintptr(pfd.FieldByName("Sysfd").Int()), nil 196 | } 197 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pro-bing 2 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/prometheus-community/pro-bing)](https://pkg.go.dev/github.com/prometheus-community/pro-bing) 3 | [![Circle CI](https://circleci.com/gh/prometheus-community/pro-bing.svg?style=svg)](https://circleci.com/gh/prometheus-community/pro-bing) 4 | 5 | A simple but powerful ICMP echo (ping) library for Go, inspired by 6 | [go-ping](https://github.com/go-ping/ping) & [go-fastping](https://github.com/tatsushid/go-fastping). 7 | 8 | Here is a very simple example that sends and receives three packets: 9 | 10 | ```go 11 | pinger, err := probing.NewPinger("www.google.com") 12 | if err != nil { 13 | panic(err) 14 | } 15 | pinger.Count = 3 16 | err = pinger.Run() // Blocks until finished. 17 | if err != nil { 18 | panic(err) 19 | } 20 | stats := pinger.Statistics() // get send/receive/duplicate/rtt stats 21 | ``` 22 | 23 | Here is an example that emulates the traditional UNIX ping command: 24 | 25 | ```go 26 | pinger, err := probing.NewPinger("www.google.com") 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | // Listen for Ctrl-C. 32 | c := make(chan os.Signal, 1) 33 | signal.Notify(c, os.Interrupt) 34 | go func() { 35 | for _ = range c { 36 | pinger.Stop() 37 | } 38 | }() 39 | 40 | pinger.OnRecv = func(pkt *probing.Packet) { 41 | fmt.Printf("%d bytes from %s: icmp_seq=%d time=%v\n", 42 | pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt) 43 | } 44 | 45 | pinger.OnDuplicateRecv = func(pkt *probing.Packet) { 46 | fmt.Printf("%d bytes from %s: icmp_seq=%d time=%v ttl=%v (DUP!)\n", 47 | pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt, pkt.TTL) 48 | } 49 | 50 | pinger.OnFinish = func(stats *probing.Statistics) { 51 | fmt.Printf("\n--- %s ping statistics ---\n", stats.Addr) 52 | fmt.Printf("%d packets transmitted, %d packets received, %v%% packet loss\n", 53 | stats.PacketsSent, stats.PacketsRecv, stats.PacketLoss) 54 | fmt.Printf("round-trip min/avg/max/stddev = %v/%v/%v/%v\n", 55 | stats.MinRtt, stats.AvgRtt, stats.MaxRtt, stats.StdDevRtt) 56 | } 57 | 58 | fmt.Printf("PING %s (%s):\n", pinger.Addr(), pinger.IPAddr()) 59 | err = pinger.Run() 60 | if err != nil { 61 | panic(err) 62 | } 63 | ``` 64 | 65 | It sends ICMP Echo Request packet(s) and waits for an Echo Reply in 66 | response. If it receives a response, it calls the `OnRecv` callback 67 | unless a packet with that sequence number has already been received, 68 | in which case it calls the `OnDuplicateRecv` callback. When it's 69 | finished, it calls the `OnFinish` callback. 70 | 71 | For a full ping example, see 72 | [cmd/ping/ping.go](https://github.com/prometheus-community/pro-bing/blob/master/cmd/ping/ping.go). 73 | 74 | ## Installation 75 | 76 | ``` 77 | go get -u github.com/prometheus-community/pro-bing 78 | ``` 79 | 80 | To install the native Go ping executable: 81 | 82 | ```bash 83 | go get -u github.com/prometheus-community/pro-bing/... 84 | $GOPATH/bin/ping 85 | ``` 86 | 87 | ## Supported Operating Systems 88 | 89 | ### Linux 90 | This library attempts to send an "unprivileged" ping via UDP. On Linux, 91 | this must be enabled with the following sysctl command: 92 | 93 | ``` 94 | sudo sysctl -w net.ipv4.ping_group_range="0 2147483647" 95 | ``` 96 | 97 | If you do not wish to do this, you can call `pinger.SetPrivileged(true)` 98 | in your code and then use setcap on your binary to allow it to bind to 99 | raw sockets (or just run it as root): 100 | 101 | ``` 102 | setcap cap_net_raw=+ep /path/to/your/compiled/binary 103 | ``` 104 | 105 | See [this blog](https://sturmflut.github.io/linux/ubuntu/2015/01/17/unprivileged-icmp-sockets-on-linux/) 106 | and the Go [x/net/icmp](https://godoc.org/golang.org/x/net/icmp) package 107 | for more details. 108 | 109 | This library supports setting the `SO_MARK` socket option which is equivalent to the `-m mark` 110 | flag in standard ping binaries on linux. Setting this option requires the `CAP_NET_ADMIN` capability 111 | (via `setcap` or elevated privileges). You can set a mark (ex: 100) with `pinger.SetMark(100)` in your code. 112 | 113 | Setting the "Don't Fragment" bit is supported under Linux which is equivalent to `ping -Mdo`. 114 | You can enable this with `pinger.SetDoNotFragment(true)`. 115 | 116 | ### Windows 117 | 118 | You must use `pinger.SetPrivileged(true)`, otherwise you will receive 119 | the following error: 120 | 121 | ``` 122 | socket: The requested protocol has not been configured into the system, or no implementation for it exists. 123 | ``` 124 | 125 | Despite the method name, this should work without the need to elevate 126 | privileges and has been tested on Windows 10. Please note that accessing 127 | packet TTL values is not supported due to limitations in the Go 128 | x/net/ipv4 and x/net/ipv6 packages. 129 | 130 | ### Plan 9 from Bell Labs 131 | 132 | There is no support for Plan 9. This is because the entire `x/net/ipv4` 133 | and `x/net/ipv6` packages are not implemented by the Go programming 134 | language. 135 | 136 | ## HTTP 137 | 138 | This library also provides support for HTTP probing. 139 | Here is a trivial example: 140 | 141 | ```go 142 | httpCaller := probing.NewHttpCaller("https://www.google.com", 143 | probing.WithHTTPCallerCallFrequency(time.Second), 144 | probing.WithHTTPCallerOnResp(func(suite *probing.TraceSuite, info *probing.HTTPCallInfo) { 145 | fmt.Printf("got resp, status code: %d, latency: %s\n", 146 | info.StatusCode, 147 | suite.GetGeneralEnd().Sub(suite.GetGeneralStart()), 148 | ) 149 | }), 150 | ) 151 | 152 | // Listen for Ctrl-C. 153 | c := make(chan os.Signal, 1) 154 | signal.Notify(c, os.Interrupt) 155 | go func() { 156 | <-c 157 | httpCaller.Stop() 158 | }() 159 | httpCaller.Run() 160 | ``` 161 | 162 | Library provides a rich list of options available for a probing. You can check the full list of available 163 | options in a generated doc. 164 | 165 | ### Callbacks 166 | 167 | HTTPCaller uses `net/http/httptrace` pkg to provide an API to track specific request event, e.g. tls handshake start. 168 | It is highly recommended to check the httptrace library [doc](https://pkg.go.dev/net/http/httptrace) to understand 169 | the purpose of provided callbacks. Nevertheless, httptrace callbacks are concurrent-unsafe, our implementation provides 170 | a concurrent-safe API. In addition to that, each callback contains a TraceSuite object which provides an Extra field 171 | 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 172 | corresponding callback. 173 | 174 | ### Target RPS & performance 175 | 176 | Library provides two options, allowing to manipulate your call load: `callFrequency` & `maxConcurrentCalls`. 177 | In case you set `callFrequency` to a value X, but it can't be achieved during the execution - you will need to 178 | try increasing a number of `maxConcurrentCalls`. Moreover, your callbacks might directly influence an execution 179 | performance. 180 | 181 | For a full documentation, please refer to the generated [doc](https://pkg.go.dev/github.com/prometheus-community/pro-bing). 182 | 183 | ## Maintainers and Getting Help: 184 | 185 | This repo was originally in the personal account of 186 | [sparrc](https://github.com/sparrc), but is now maintained by the 187 | [Prometheus Community](https://prometheus.io/community). 188 | 189 | ## Contributing 190 | 191 | Refer to [CONTRIBUTING.md](https://github.com/prometheus-community/pro-bing/blob/master/CONTRIBUTING.md) 192 | -------------------------------------------------------------------------------- /http_test.go: -------------------------------------------------------------------------------- 1 | package probing 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "net/http" 7 | "net/http/httptest" 8 | "runtime/debug" 9 | "sync" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | // TODO: figure out how to test onDNS callback 15 | func getTestHTTPClientServer() (*http.Client, *httptest.Server) { 16 | srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 17 | w.WriteHeader(http.StatusOK) 18 | })) 19 | return srv.Client(), srv 20 | } 21 | 22 | func TestHTTPCaller_MakeCall_OnReq(t *testing.T) { 23 | client, srv := getTestHTTPClientServer() 24 | defer srv.Close() 25 | 26 | var callbackCalled bool 27 | httpCaller := NewHttpCaller(srv.URL, 28 | WithHTTPCallerClient(client), 29 | WithHTTPCallerOnReq(func(suite *TraceSuite) { 30 | AssertTimeNonZero(t, suite.generalStart) 31 | AssertTimeZero(t, suite.generalEnd) 32 | AssertTimeZero(t, suite.connStart) 33 | AssertTimeZero(t, suite.connEnd) 34 | AssertTimeZero(t, suite.tlsStart) 35 | AssertTimeZero(t, suite.tlsEnd) 36 | AssertTimeZero(t, suite.wroteHeaders) 37 | AssertTimeZero(t, suite.firstByteReceived) 38 | callbackCalled = true 39 | })) 40 | err := httpCaller.makeCall(context.Background()) 41 | AssertNoError(t, err) 42 | AssertTrue(t, callbackCalled) 43 | } 44 | 45 | func TestHTTPCaller_MakeCall_OnConnStart(t *testing.T) { 46 | client, srv := getTestHTTPClientServer() 47 | defer srv.Close() 48 | 49 | var callbackCalled bool 50 | httpCaller := NewHttpCaller(srv.URL, 51 | WithHTTPCallerClient(client), 52 | WithHTTPCallerOnConnStart(func(suite *TraceSuite, network, addr string) { 53 | AssertTimeNonZero(t, suite.generalStart) 54 | AssertTimeZero(t, suite.generalEnd) 55 | AssertTimeNonZero(t, suite.connStart) 56 | AssertTimeZero(t, suite.connEnd) 57 | AssertTimeZero(t, suite.tlsStart) 58 | AssertTimeZero(t, suite.tlsEnd) 59 | AssertTimeZero(t, suite.wroteHeaders) 60 | AssertTimeZero(t, suite.firstByteReceived) 61 | callbackCalled = true 62 | })) 63 | err := httpCaller.makeCall(context.Background()) 64 | AssertNoError(t, err) 65 | AssertTrue(t, callbackCalled) 66 | } 67 | 68 | func TestHTTPCaller_MakeCall_OnConnDone(t *testing.T) { 69 | client, srv := getTestHTTPClientServer() 70 | defer srv.Close() 71 | 72 | var callbackCalled bool 73 | httpCaller := NewHttpCaller(srv.URL, 74 | WithHTTPCallerClient(client), 75 | WithHTTPCallerOnConnDone(func(suite *TraceSuite, network, addr string, err error) { 76 | AssertTimeNonZero(t, suite.generalStart) 77 | AssertTimeZero(t, suite.generalEnd) 78 | AssertTimeNonZero(t, suite.connStart) 79 | AssertTimeNonZero(t, suite.connEnd) 80 | AssertTimeZero(t, suite.tlsStart) 81 | AssertTimeZero(t, suite.tlsEnd) 82 | AssertTimeZero(t, suite.wroteHeaders) 83 | AssertTimeZero(t, suite.firstByteReceived) 84 | callbackCalled = true 85 | })) 86 | err := httpCaller.makeCall(context.Background()) 87 | AssertNoError(t, err) 88 | AssertTrue(t, callbackCalled) 89 | } 90 | 91 | func TestHTTPCaller_MakeCall_OnTLSStart(t *testing.T) { 92 | client, srv := getTestHTTPClientServer() 93 | defer srv.Close() 94 | 95 | var callbackCalled bool 96 | httpCaller := NewHttpCaller(srv.URL, 97 | WithHTTPCallerClient(client), 98 | WithHTTPCallerOnTLSStart(func(suite *TraceSuite) { 99 | AssertTimeNonZero(t, suite.generalStart) 100 | AssertTimeZero(t, suite.generalEnd) 101 | AssertTimeNonZero(t, suite.connStart) 102 | AssertTimeNonZero(t, suite.connEnd) 103 | AssertTimeNonZero(t, suite.tlsStart) 104 | AssertTimeZero(t, suite.tlsEnd) 105 | AssertTimeZero(t, suite.wroteHeaders) 106 | AssertTimeZero(t, suite.firstByteReceived) 107 | callbackCalled = true 108 | })) 109 | err := httpCaller.makeCall(context.Background()) 110 | AssertNoError(t, err) 111 | AssertTrue(t, callbackCalled) 112 | } 113 | 114 | func TestHTTPCaller_MakeCall_OnTLSDone(t *testing.T) { 115 | client, srv := getTestHTTPClientServer() 116 | defer srv.Close() 117 | 118 | var callbackCalled bool 119 | httpCaller := NewHttpCaller(srv.URL, 120 | WithHTTPCallerClient(client), 121 | WithHTTPCallerOnTLSDone(func(suite *TraceSuite, state tls.ConnectionState, err error) { 122 | AssertTimeNonZero(t, suite.generalStart) 123 | AssertTimeZero(t, suite.generalEnd) 124 | AssertTimeNonZero(t, suite.connStart) 125 | AssertTimeNonZero(t, suite.connEnd) 126 | AssertTimeNonZero(t, suite.tlsStart) 127 | AssertTimeNonZero(t, suite.tlsEnd) 128 | AssertTimeZero(t, suite.wroteHeaders) 129 | AssertTimeZero(t, suite.firstByteReceived) 130 | callbackCalled = true 131 | })) 132 | err := httpCaller.makeCall(context.Background()) 133 | AssertNoError(t, err) 134 | AssertTrue(t, callbackCalled) 135 | } 136 | 137 | func TestHTTPCaller_MakeCall_OnWroteHeaders(t *testing.T) { 138 | client, srv := getTestHTTPClientServer() 139 | defer srv.Close() 140 | 141 | var callbackCalled bool 142 | httpCaller := NewHttpCaller(srv.URL, 143 | WithHTTPCallerClient(client), 144 | WithHTTPCallerOnWroteRequest(func(suite *TraceSuite) { 145 | AssertTimeNonZero(t, suite.generalStart) 146 | AssertTimeZero(t, suite.generalEnd) 147 | AssertTimeNonZero(t, suite.connStart) 148 | AssertTimeNonZero(t, suite.connEnd) 149 | AssertTimeNonZero(t, suite.tlsStart) 150 | AssertTimeNonZero(t, suite.tlsEnd) 151 | AssertTimeNonZero(t, suite.wroteHeaders) 152 | AssertTimeZero(t, suite.firstByteReceived) 153 | callbackCalled = true 154 | })) 155 | err := httpCaller.makeCall(context.Background()) 156 | AssertNoError(t, err) 157 | AssertTrue(t, callbackCalled) 158 | } 159 | 160 | func TestHTTPCaller_MakeCall_OnFirstByteReceived(t *testing.T) { 161 | client, srv := getTestHTTPClientServer() 162 | defer srv.Close() 163 | 164 | var callbackCalled bool 165 | httpCaller := NewHttpCaller(srv.URL, 166 | WithHTTPCallerClient(client), 167 | WithHTTPCallerOnFirstByteReceived(func(suite *TraceSuite) { 168 | AssertTimeNonZero(t, suite.generalStart) 169 | AssertTimeZero(t, suite.generalEnd) 170 | AssertTimeNonZero(t, suite.connStart) 171 | AssertTimeNonZero(t, suite.connEnd) 172 | AssertTimeNonZero(t, suite.tlsStart) 173 | AssertTimeNonZero(t, suite.tlsEnd) 174 | AssertTimeNonZero(t, suite.wroteHeaders) 175 | AssertTimeNonZero(t, suite.firstByteReceived) 176 | callbackCalled = true 177 | })) 178 | err := httpCaller.makeCall(context.Background()) 179 | AssertNoError(t, err) 180 | AssertTrue(t, callbackCalled) 181 | } 182 | 183 | func TestHTTPCaller_MakeCall_OnResp(t *testing.T) { 184 | client, srv := getTestHTTPClientServer() 185 | defer srv.Close() 186 | 187 | var callbackCalled bool 188 | httpCaller := NewHttpCaller(srv.URL, 189 | WithHTTPCallerClient(client), 190 | WithHTTPCallerOnResp(func(suite *TraceSuite, info *HTTPCallInfo) { 191 | AssertTimeNonZero(t, suite.generalStart) 192 | AssertTimeNonZero(t, suite.generalEnd) 193 | AssertTimeNonZero(t, suite.connStart) 194 | AssertTimeNonZero(t, suite.connEnd) 195 | AssertTimeNonZero(t, suite.tlsStart) 196 | AssertTimeNonZero(t, suite.tlsEnd) 197 | AssertTimeNonZero(t, suite.wroteHeaders) 198 | AssertTimeNonZero(t, suite.firstByteReceived) 199 | callbackCalled = true 200 | })) 201 | err := httpCaller.makeCall(context.Background()) 202 | AssertNoError(t, err) 203 | AssertTrue(t, callbackCalled) 204 | } 205 | 206 | func TestHTTPCaller_MakeCall_IsValidResponse(t *testing.T) { 207 | client, srv := getTestHTTPClientServer() 208 | defer srv.Close() 209 | 210 | t.Run("no callback", func(t *testing.T) { 211 | var callbackCalled bool 212 | httpCaller := NewHttpCaller(srv.URL, 213 | WithHTTPCallerClient(client), 214 | WithHTTPCallerOnResp(func(suite *TraceSuite, info *HTTPCallInfo) { 215 | AssertTrue(t, info.IsValidResponse) 216 | callbackCalled = true 217 | }), 218 | ) 219 | err := httpCaller.makeCall(context.Background()) 220 | AssertNoError(t, err) 221 | AssertTrue(t, callbackCalled) 222 | }) 223 | 224 | t.Run("false callback", func(t *testing.T) { 225 | var callbackCalled bool 226 | httpCaller := NewHttpCaller(srv.URL, 227 | WithHTTPCallerClient(client), 228 | WithHTTPCallerIsValidResponse(func(response *http.Response, body []byte) bool { 229 | return false 230 | }), 231 | WithHTTPCallerOnResp(func(suite *TraceSuite, info *HTTPCallInfo) { 232 | AssertFalse(t, info.IsValidResponse) 233 | callbackCalled = true 234 | }), 235 | ) 236 | err := httpCaller.makeCall(context.Background()) 237 | AssertNoError(t, err) 238 | AssertTrue(t, callbackCalled) 239 | }) 240 | 241 | t.Run("true callback", func(t *testing.T) { 242 | var callbackCalled bool 243 | httpCaller := NewHttpCaller(srv.URL, 244 | WithHTTPCallerClient(client), 245 | WithHTTPCallerIsValidResponse(func(response *http.Response, body []byte) bool { 246 | return true 247 | }), 248 | WithHTTPCallerOnResp(func(suite *TraceSuite, info *HTTPCallInfo) { 249 | AssertTrue(t, info.IsValidResponse) 250 | callbackCalled = true 251 | }), 252 | ) 253 | err := httpCaller.makeCall(context.Background()) 254 | AssertNoError(t, err) 255 | AssertTrue(t, callbackCalled) 256 | }) 257 | } 258 | 259 | func TestHTTPCaller_RunWithContext(t *testing.T) { 260 | client, srv := getTestHTTPClientServer() 261 | defer srv.Close() 262 | 263 | var callsCount int 264 | var callsCountMu sync.Mutex 265 | httpCaller := NewHttpCaller(srv.URL, 266 | WithHTTPCallerMaxConcurrentCalls(5), 267 | WithHTTPCallerCallFrequency(time.Second/5), 268 | WithHTTPCallerClient(client), 269 | WithHTTPCallerTimeout(time.Second), 270 | WithHTTPCallerOnReq(func(suite *TraceSuite) { 271 | callsCountMu.Lock() 272 | defer callsCountMu.Unlock() 273 | callsCount++ 274 | }), 275 | ) 276 | ctx := context.Background() 277 | go func() { 278 | httpCaller.RunWithContext(ctx) 279 | }() 280 | time.Sleep(time.Second) 281 | done := make(chan struct{}) 282 | go func() { 283 | ctx.Done() 284 | done <- struct{}{} 285 | }() 286 | select { 287 | case <-time.After(time.Second * 2): 288 | t.Errorf("Timed out on a shutdown, meaning workload wasn't processed, Stack: \n%s", string(debug.Stack())) 289 | case <-done: 290 | } 291 | httpCaller.Stop() 292 | AssertIntGreaterOrEqual(t, callsCount, 5) 293 | } 294 | 295 | func AssertTimeZero(t *testing.T, tm time.Time) { 296 | t.Helper() 297 | if !tm.IsZero() { 298 | t.Errorf("Expected zero time, got non zero time, Stack: \n%s", string(debug.Stack())) 299 | } 300 | } 301 | 302 | func AssertTimeNonZero(t *testing.T, tm time.Time) { 303 | t.Helper() 304 | if tm.IsZero() { 305 | t.Errorf("Expected non zero time, got zero time, Stack: \n%s", string(debug.Stack())) 306 | } 307 | } 308 | 309 | func AssertIntGreaterOrEqual(t *testing.T, expected, actual int) { 310 | t.Helper() 311 | if actual < expected { 312 | t.Errorf("Exptected value to be less then %v, got %v, Stack: \n%s", expected, actual, string(debug.Stack())) 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package probing 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/tls" 7 | "io" 8 | "net/http" 9 | "net/http/httptrace" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | const ( 15 | defaultHTTPCallFrequency = time.Second 16 | defaultHTTPMaxConcurrentCalls = 1 17 | defaultHTTPMethod = http.MethodGet 18 | defaultTimeout = time.Second * 10 19 | ) 20 | 21 | type httpCallerOptions struct { 22 | client *http.Client 23 | 24 | callFrequency time.Duration 25 | maxConcurrentCalls int 26 | 27 | host string 28 | headers http.Header 29 | method string 30 | body []byte 31 | timeout time.Duration 32 | 33 | isValidResponse func(response *http.Response, body []byte) bool 34 | 35 | onDNSStart func(suite *TraceSuite, info httptrace.DNSStartInfo) 36 | onDNSDone func(suite *TraceSuite, info httptrace.DNSDoneInfo) 37 | onConnStart func(suite *TraceSuite, network, addr string) 38 | onConnDone func(suite *TraceSuite, network, addr string, err error) 39 | onTLSStart func(suite *TraceSuite) 40 | onTLSDone func(suite *TraceSuite, state tls.ConnectionState, err error) 41 | onWroteHeaders func(suite *TraceSuite) 42 | onFirstByteReceived func(suite *TraceSuite) 43 | onReq func(suite *TraceSuite) 44 | onResp func(suite *TraceSuite, info *HTTPCallInfo) 45 | 46 | logger Logger 47 | } 48 | 49 | // HTTPCallerOption represents a function type for a functional parameter passed to a NewHttpCaller constructor. 50 | type HTTPCallerOption func(options *httpCallerOptions) 51 | 52 | // WithHTTPCallerClient is a functional parameter for a HTTPCaller which specifies a http.Client. 53 | func WithHTTPCallerClient(client *http.Client) HTTPCallerOption { 54 | return func(options *httpCallerOptions) { 55 | options.client = client 56 | } 57 | } 58 | 59 | // WithHTTPCallerCallFrequency is a functional parameter for a HTTPCaller which specifies a call frequency. 60 | // If this option is not provided the default one will be used. You can check default value in const 61 | // defaultHTTPCallFrequency. 62 | func WithHTTPCallerCallFrequency(frequency time.Duration) HTTPCallerOption { 63 | return func(options *httpCallerOptions) { 64 | options.callFrequency = frequency 65 | } 66 | } 67 | 68 | // WithHTTPCallerMaxConcurrentCalls is a functional parameter for a HTTPCaller which specifies a number of 69 | // maximum concurrent calls. If this option is not provided the default one will be used. You can check default value in const 70 | // defaultHTTPMaxConcurrentCalls. 71 | func WithHTTPCallerMaxConcurrentCalls(max int) HTTPCallerOption { 72 | return func(options *httpCallerOptions) { 73 | options.maxConcurrentCalls = max 74 | } 75 | } 76 | 77 | // WithHTTPCallerHeaders is a functional parameter for a HTTPCaller which specifies headers that should be 78 | // set in request. 79 | // To override a Host header use a WithHTTPCallerHost method. 80 | func WithHTTPCallerHeaders(headers http.Header) HTTPCallerOption { 81 | return func(options *httpCallerOptions) { 82 | options.headers = headers 83 | } 84 | } 85 | 86 | // WithHTTPCallerMethod is a functional parameter for a HTTPCaller which specifies a method that should be 87 | // set in request. If this option is not provided the default one will be used. You can check default value in const 88 | // defaultHTTPMethod. 89 | func WithHTTPCallerMethod(method string) HTTPCallerOption { 90 | return func(options *httpCallerOptions) { 91 | options.method = method 92 | } 93 | } 94 | 95 | // WithHTTPCallerHost is a functional parameter for a HTTPCaller which allowed to override a host header. 96 | func WithHTTPCallerHost(host string) HTTPCallerOption { 97 | return func(options *httpCallerOptions) { 98 | options.host = host 99 | } 100 | } 101 | 102 | // WithHTTPCallerBody is a functional parameter for a HTTPCaller which specifies a body that should be set 103 | // in request. 104 | func WithHTTPCallerBody(body []byte) HTTPCallerOption { 105 | return func(options *httpCallerOptions) { 106 | options.body = body 107 | } 108 | } 109 | 110 | // WithHTTPCallerTimeout is a functional parameter for a HTTPCaller which specifies request timeout. 111 | // If this option is not provided the default one will be used. You can check default value in const defaultTimeout. 112 | func WithHTTPCallerTimeout(timeout time.Duration) HTTPCallerOption { 113 | return func(options *httpCallerOptions) { 114 | options.timeout = timeout 115 | } 116 | } 117 | 118 | // WithHTTPCallerIsValidResponse is a functional parameter for a HTTPCaller which specifies a function that 119 | // will be used to assess whether a response is valid. If not specified, all responses will be treated as valid. 120 | // You can read more explanation about this parameter in HTTPCaller annotation. 121 | func WithHTTPCallerIsValidResponse(isValid func(response *http.Response, body []byte) bool) HTTPCallerOption { 122 | return func(options *httpCallerOptions) { 123 | options.isValidResponse = isValid 124 | } 125 | } 126 | 127 | // WithHTTPCallerOnDNSStart is a functional parameter for a HTTPCaller which specifies a callback that will be 128 | // called when dns resolving starts. You can read more explanation about this parameter in HTTPCaller annotation. 129 | func WithHTTPCallerOnDNSStart(onDNSStart func(suite *TraceSuite, info httptrace.DNSStartInfo)) HTTPCallerOption { 130 | return func(options *httpCallerOptions) { 131 | options.onDNSStart = onDNSStart 132 | } 133 | } 134 | 135 | // WithHTTPCallerOnDNSDone is a functional parameter for a HTTPCaller which specifies a callback that will be 136 | // called when dns resolving ended. You can read more explanation about this parameter in HTTPCaller annotation. 137 | func WithHTTPCallerOnDNSDone(onDNSDone func(suite *TraceSuite, info httptrace.DNSDoneInfo)) HTTPCallerOption { 138 | return func(options *httpCallerOptions) { 139 | options.onDNSDone = onDNSDone 140 | } 141 | } 142 | 143 | // WithHTTPCallerOnConnStart is a functional parameter for a HTTPCaller which specifies a callback that will be 144 | // called when connection establishment started. You can read more explanation about this parameter in HTTPCaller 145 | // annotation. 146 | func WithHTTPCallerOnConnStart(onConnStart func(suite *TraceSuite, network, addr string)) HTTPCallerOption { 147 | return func(options *httpCallerOptions) { 148 | options.onConnStart = onConnStart 149 | } 150 | } 151 | 152 | // WithHTTPCallerOnConnDone is a functional parameter for a HTTPCaller which specifies a callback that will be 153 | // called when connection establishment finished. You can read more explanation about this parameter in HTTPCaller 154 | // annotation. 155 | func WithHTTPCallerOnConnDone(conConnDone func(suite *TraceSuite, network, addr string, err error)) HTTPCallerOption { 156 | return func(options *httpCallerOptions) { 157 | options.onConnDone = conConnDone 158 | } 159 | } 160 | 161 | // WithHTTPCallerOnTLSStart is a functional parameter for a HTTPCaller which specifies a callback that will be 162 | // called when tls handshake started. You can read more explanation about this parameter in HTTPCaller annotation. 163 | func WithHTTPCallerOnTLSStart(onTLSStart func(suite *TraceSuite)) HTTPCallerOption { 164 | return func(options *httpCallerOptions) { 165 | options.onTLSStart = onTLSStart 166 | } 167 | } 168 | 169 | // WithHTTPCallerOnTLSDone is a functional parameter for a HTTPCaller which specifies a callback that will be 170 | // called when tls handshake ended. You can read more explanation about this parameter in HTTPCaller annotation. 171 | func WithHTTPCallerOnTLSDone(onTLSDone func(suite *TraceSuite, state tls.ConnectionState, err error)) HTTPCallerOption { 172 | return func(options *httpCallerOptions) { 173 | options.onTLSDone = onTLSDone 174 | } 175 | } 176 | 177 | // WithHTTPCallerOnWroteRequest is a functional parameter for a HTTPCaller which specifies a callback that will be 178 | // called when request has been written. You can read more explanation about this parameter in HTTPCaller annotation. 179 | func WithHTTPCallerOnWroteRequest(onWroteRequest func(suite *TraceSuite)) HTTPCallerOption { 180 | return func(options *httpCallerOptions) { 181 | options.onWroteHeaders = onWroteRequest 182 | } 183 | } 184 | 185 | // WithHTTPCallerOnFirstByteReceived is a functional parameter for a HTTPCaller which specifies a callback that will be 186 | // called when first response byte has been received. You can read more explanation about this parameter in HTTPCaller 187 | // annotation. 188 | func WithHTTPCallerOnFirstByteReceived(onGotFirstByte func(suite *TraceSuite)) HTTPCallerOption { 189 | return func(options *httpCallerOptions) { 190 | options.onFirstByteReceived = onGotFirstByte 191 | } 192 | } 193 | 194 | // WithHTTPCallerOnReq is a functional parameter for a HTTPCaller which specifies a callback that will be 195 | // called before the start of the http call execution. You can read more explanation about this parameter in HTTPCaller 196 | // annotation. 197 | func WithHTTPCallerOnReq(onReq func(suite *TraceSuite)) HTTPCallerOption { 198 | return func(options *httpCallerOptions) { 199 | options.onReq = onReq 200 | } 201 | } 202 | 203 | // WithHTTPCallerOnResp is a functional parameter for a HTTPCaller which specifies a callback that will be 204 | // called when response is received. You can read more explanation about this parameter in HTTPCaller annotation. 205 | func WithHTTPCallerOnResp(onResp func(suite *TraceSuite, info *HTTPCallInfo)) HTTPCallerOption { 206 | return func(options *httpCallerOptions) { 207 | options.onResp = onResp 208 | } 209 | } 210 | 211 | // WithHTTPCallerLogger is a functional parameter for a HTTPCaller which specifies a logger. 212 | // If not specified, logs will be omitted. 213 | func WithHTTPCallerLogger(logger Logger) HTTPCallerOption { 214 | return func(options *httpCallerOptions) { 215 | options.logger = logger 216 | } 217 | } 218 | 219 | // NewHttpCaller returns a new HTTPCaller. URL parameter is the only required one, other options might be specified via 220 | // functional parameters, otherwise default values will be used where applicable. 221 | func NewHttpCaller(url string, options ...HTTPCallerOption) *HTTPCaller { 222 | opts := httpCallerOptions{ 223 | callFrequency: defaultHTTPCallFrequency, 224 | maxConcurrentCalls: defaultHTTPMaxConcurrentCalls, 225 | method: defaultHTTPMethod, 226 | timeout: defaultTimeout, 227 | client: &http.Client{}, 228 | } 229 | for _, opt := range options { 230 | opt(&opts) 231 | } 232 | 233 | return &HTTPCaller{ 234 | client: opts.client, 235 | 236 | callFrequency: opts.callFrequency, 237 | maxConcurrentCalls: opts.maxConcurrentCalls, 238 | 239 | url: url, 240 | host: opts.host, 241 | headers: opts.headers, 242 | method: opts.method, 243 | body: opts.body, 244 | timeout: opts.timeout, 245 | 246 | isValidResponse: opts.isValidResponse, 247 | 248 | workChan: make(chan struct{}, opts.maxConcurrentCalls), 249 | doneChan: make(chan struct{}), 250 | 251 | onDNSStart: opts.onDNSStart, 252 | onDNSDone: opts.onDNSDone, 253 | onConnStart: opts.onConnStart, 254 | onConnDone: opts.onConnDone, 255 | onTLSStart: opts.onTLSStart, 256 | onTLSDone: opts.onTLSDone, 257 | onWroteHeaders: opts.onWroteHeaders, 258 | onFirstByteReceived: opts.onFirstByteReceived, 259 | onReq: opts.onReq, 260 | onResp: opts.onResp, 261 | 262 | logger: opts.logger, 263 | } 264 | } 265 | 266 | // HTTPCaller represents a prober performing http calls and collecting relevant statistics. 267 | type HTTPCaller struct { 268 | client *http.Client 269 | 270 | // callFrequency is a parameter which specifies how often to send a new request. You might need to increase 271 | // maxConcurrentCalls value to achieve required value. 272 | callFrequency time.Duration 273 | 274 | // maxConcurrentCalls is a maximum number of calls that might be performed concurrently. In other words, 275 | // a number of "workers" that will try to perform probing concurrently. 276 | // Default number is specified in defaultHTTPMaxConcurrentCalls 277 | maxConcurrentCalls int 278 | 279 | // url is an url which will be used in all probe requests, mandatory in constructor. 280 | url string 281 | 282 | // host allows to override a Host header 283 | host string 284 | 285 | // headers are headers that which will be used in all probe requests, default are none. 286 | headers http.Header 287 | 288 | // method is a http request method which will be used in all probe requests, 289 | // default is specified in defaultHTTPMethod 290 | method string 291 | 292 | // body is a http request body which will be used in all probe requests, default is none. 293 | body []byte 294 | 295 | // timeout is a http call timeout, default is specified in defaultTimeout. 296 | timeout time.Duration 297 | 298 | // isValidResponse is a function that will be used to validate whether a response is valid up to clients choice. 299 | // You can think of it as a verification that response contains data that you expected. This information will be 300 | // passed back in HTTPCallInfo during an onResp callback and HTTPStatistics during an onFinish callback 301 | // or a Statistics call. 302 | isValidResponse func(response *http.Response, body []byte) bool 303 | 304 | workChan chan struct{} 305 | doneChan chan struct{} 306 | doneWg sync.WaitGroup 307 | 308 | // All callbacks except onReq and onResp are based on a httptrace callbacks, meaning they are called at the time 309 | // and contain signature same as you would expect in httptrace library. In addition to that each callback has a 310 | // TraceSuite as a first argument, which will help you to propagate data between these callbacks. You can read more 311 | // about it in TraceSuite annotation. 312 | 313 | // onDNSStart is a callback which is called when a dns lookup starts. It's based on a httptrace.DNSStart callback. 314 | onDNSStart func(suite *TraceSuite, info httptrace.DNSStartInfo) 315 | // onDNSDone is a callback which is called when a dns lookup ends. It's based on a httptrace.DNSDone callback. 316 | onDNSDone func(suite *TraceSuite, info httptrace.DNSDoneInfo) 317 | // onConnStart is a callback which is called when a connection dial starts. It's based on a httptrace.ConnectStart 318 | // callback. 319 | onConnStart func(suite *TraceSuite, network, addr string) 320 | // onConnDone is a callback which is called when a connection dial ends. It's based on a httptrace.ConnectDone 321 | // callback. 322 | onConnDone func(suite *TraceSuite, network, addr string, err error) 323 | // onTLSStart is a callback which is called when a tls handshake starts. It's based on a httptrace.TLSHandshakeStart 324 | // callback. 325 | onTLSStart func(suite *TraceSuite) 326 | // onTLSDone is a callback which is called when a tls handshake ends. It's based on a httptrace.TLSHandshakeDone 327 | // callback. 328 | onTLSDone func(suite *TraceSuite, state tls.ConnectionState, err error) 329 | // onWroteHeaders is a callback which is called when request headers where written. It's based on a 330 | // httptrace.WroteHeaders callback. 331 | onWroteHeaders func(suite *TraceSuite) 332 | // onFirstByteReceived is a callback which is called when first response bytes were received. It's based on a 333 | // httptrace.GotFirstResponseByte callback. 334 | onFirstByteReceived func(suite *TraceSuite) 335 | 336 | // onReq is a custom callback which is called before http client starts request execution. 337 | onReq func(suite *TraceSuite) 338 | // onResp is a custom callback which is called when a response is received. 339 | onResp func(suite *TraceSuite, info *HTTPCallInfo) 340 | 341 | // logger is a logger implementation, default is none. 342 | logger Logger 343 | } 344 | 345 | // Stop gracefully stops the execution of a HTTPCaller. 346 | func (c *HTTPCaller) Stop() { 347 | close(c.doneChan) 348 | c.doneWg.Wait() 349 | } 350 | 351 | // Run starts execution of a probing. 352 | func (c *HTTPCaller) Run() { 353 | c.run(context.Background()) 354 | } 355 | 356 | // RunWithContext starts execution of a probing and allows providing a context. 357 | func (c *HTTPCaller) RunWithContext(ctx context.Context) { 358 | c.run(ctx) 359 | } 360 | 361 | func (c *HTTPCaller) run(ctx context.Context) { 362 | c.runWorkScheduler(ctx) 363 | c.runCallers(ctx) 364 | c.doneWg.Wait() 365 | } 366 | 367 | func (c *HTTPCaller) runWorkScheduler(ctx context.Context) { 368 | c.doneWg.Add(1) 369 | go func() { 370 | defer c.doneWg.Done() 371 | 372 | ticker := time.NewTicker(c.callFrequency) 373 | defer ticker.Stop() 374 | 375 | for { 376 | select { 377 | case <-ticker.C: 378 | c.workChan <- struct{}{} 379 | case <-ctx.Done(): 380 | return 381 | case <-c.doneChan: 382 | return 383 | } 384 | } 385 | }() 386 | } 387 | 388 | func (c *HTTPCaller) runCallers(ctx context.Context) { 389 | for i := 0; i < c.maxConcurrentCalls; i++ { 390 | c.doneWg.Add(1) 391 | go func() { 392 | defer c.doneWg.Done() 393 | for { 394 | logger := c.logger 395 | if logger == nil { 396 | logger = NoopLogger{} 397 | } 398 | select { 399 | case <-c.workChan: 400 | if err := c.makeCall(ctx); err != nil { 401 | logger.Errorf("failed making a call: %v", err) 402 | } 403 | case <-ctx.Done(): 404 | return 405 | case <-c.doneChan: 406 | return 407 | } 408 | } 409 | }() 410 | } 411 | } 412 | 413 | // TraceSuite is a struct that is passed to each callback. It contains a bunch of time helpers, that you can use with 414 | // a corresponding getter. These timers are set before making a corresponding callback, meaning that when an onDNSStart 415 | // callback will be called - TraceSuite will already have filled dnsStart field. In addition to that, it contains 416 | // an Extra field of type any which you can use in any custom way you might need. Before each callback call, mutex 417 | // is used, meaning all operations inside your callback are concurrent-safe. 418 | // Keep in mind, that if your http client set up to follow redirects - timers will be overwritten. 419 | type TraceSuite struct { 420 | mu sync.Mutex 421 | 422 | generalStart time.Time 423 | generalEnd time.Time 424 | dnsStart time.Time 425 | dnsEnd time.Time 426 | connStart time.Time 427 | connEnd time.Time 428 | tlsStart time.Time 429 | tlsEnd time.Time 430 | wroteHeaders time.Time 431 | firstByteReceived time.Time 432 | 433 | Extra any 434 | } 435 | 436 | // GetGeneralStart returns a general http request execution start time. 437 | func (s *TraceSuite) GetGeneralStart() time.Time { 438 | return s.generalStart 439 | } 440 | 441 | // GetGeneralEnd returns a general http response time. 442 | func (s *TraceSuite) GetGeneralEnd() time.Time { 443 | return s.generalEnd 444 | } 445 | 446 | // GetDNSStart returns a time of a dns lookup start. 447 | func (s *TraceSuite) GetDNSStart() time.Time { 448 | return s.dnsStart 449 | } 450 | 451 | // GetDNSEnd returns a time of a dns lookup send. 452 | func (s *TraceSuite) GetDNSEnd() time.Time { 453 | return s.dnsEnd 454 | } 455 | 456 | // GetConnStart returns a time of a connection dial start. 457 | func (s *TraceSuite) GetConnStart() time.Time { 458 | return s.connStart 459 | } 460 | 461 | // GetConnEnd returns a time of a connection dial end. 462 | func (s *TraceSuite) GetConnEnd() time.Time { 463 | return s.connEnd 464 | } 465 | 466 | // GetTLSStart returns a time of a tls handshake start. 467 | func (s *TraceSuite) GetTLSStart() time.Time { 468 | return s.tlsStart 469 | } 470 | 471 | // GetTLSEnd returns a time of a tls handshake end. 472 | func (s *TraceSuite) GetTLSEnd() time.Time { 473 | return s.tlsEnd 474 | } 475 | 476 | // GetWroteHeaders returns a time when request headers were written. 477 | func (s *TraceSuite) GetWroteHeaders() time.Time { 478 | return s.wroteHeaders 479 | } 480 | 481 | // GetFirstByteReceived returns a time when first response bytes were received. 482 | func (s *TraceSuite) GetFirstByteReceived() time.Time { 483 | return s.firstByteReceived 484 | } 485 | 486 | func (c *HTTPCaller) getClientTrace(suite *TraceSuite) *httptrace.ClientTrace { 487 | return &httptrace.ClientTrace{ 488 | DNSStart: func(info httptrace.DNSStartInfo) { 489 | suite.mu.Lock() 490 | defer suite.mu.Unlock() 491 | 492 | suite.dnsStart = time.Now() 493 | if c.onDNSStart != nil { 494 | c.onDNSStart(suite, info) 495 | } 496 | }, 497 | DNSDone: func(info httptrace.DNSDoneInfo) { 498 | suite.mu.Lock() 499 | defer suite.mu.Unlock() 500 | 501 | suite.dnsEnd = time.Now() 502 | if c.onDNSDone != nil { 503 | c.onDNSDone(suite, info) 504 | } 505 | }, 506 | ConnectStart: func(network, addr string) { 507 | suite.mu.Lock() 508 | defer suite.mu.Unlock() 509 | 510 | suite.connStart = time.Now() 511 | if c.onConnStart != nil { 512 | c.onConnStart(suite, network, addr) 513 | } 514 | }, 515 | ConnectDone: func(network, addr string, err error) { 516 | suite.mu.Lock() 517 | defer suite.mu.Unlock() 518 | 519 | suite.connEnd = time.Now() 520 | if c.onConnDone != nil { 521 | c.onConnDone(suite, network, addr, err) 522 | } 523 | }, 524 | TLSHandshakeStart: func() { 525 | suite.mu.Lock() 526 | defer suite.mu.Unlock() 527 | 528 | suite.tlsStart = time.Now() 529 | if c.onTLSStart != nil { 530 | c.onTLSStart(suite) 531 | } 532 | }, 533 | TLSHandshakeDone: func(state tls.ConnectionState, err error) { 534 | suite.mu.Lock() 535 | defer suite.mu.Unlock() 536 | 537 | suite.tlsEnd = time.Now() 538 | if c.onTLSDone != nil { 539 | c.onTLSDone(suite, state, err) 540 | } 541 | }, 542 | WroteHeaders: func() { 543 | suite.mu.Lock() 544 | defer suite.mu.Unlock() 545 | 546 | suite.wroteHeaders = time.Now() 547 | if c.onWroteHeaders != nil { 548 | c.onWroteHeaders(suite) 549 | } 550 | }, 551 | GotFirstResponseByte: func() { 552 | suite.mu.Lock() 553 | defer suite.mu.Unlock() 554 | 555 | suite.firstByteReceived = time.Now() 556 | if c.onFirstByteReceived != nil { 557 | c.onFirstByteReceived(suite) 558 | } 559 | }, 560 | } 561 | } 562 | 563 | func (c *HTTPCaller) makeCall(ctx context.Context) error { 564 | ctx, cancel := context.WithTimeout(ctx, c.timeout) 565 | defer cancel() 566 | 567 | suite := TraceSuite{ 568 | generalStart: time.Now(), 569 | } 570 | traceCtx := httptrace.WithClientTrace(ctx, c.getClientTrace(&suite)) 571 | req, err := http.NewRequestWithContext(traceCtx, c.method, c.url, bytes.NewReader(c.body)) 572 | if err != nil { 573 | return err 574 | } 575 | req.Header = c.headers 576 | if c.host != "" { 577 | req.Host = c.host 578 | } 579 | 580 | if c.onReq != nil { 581 | suite.mu.Lock() 582 | c.onReq(&suite) 583 | suite.mu.Unlock() 584 | } 585 | resp, err := c.client.Do(req) 586 | if err != nil { 587 | return err 588 | } 589 | body, err := io.ReadAll(resp.Body) 590 | if err != nil { 591 | return err 592 | } 593 | resp.Body.Close() 594 | isValidResponse := true 595 | if c.isValidResponse != nil { 596 | isValidResponse = c.isValidResponse(resp, body) 597 | } 598 | if c.onResp != nil { 599 | suite.mu.Lock() 600 | defer suite.mu.Unlock() 601 | 602 | suite.generalEnd = time.Now() 603 | c.onResp(&suite, &HTTPCallInfo{ 604 | StatusCode: resp.StatusCode, 605 | IsValidResponse: isValidResponse, 606 | }) 607 | } 608 | return nil 609 | } 610 | 611 | // HTTPCallInfo represents a data set which passed as a function argument to an onResp callback. 612 | type HTTPCallInfo struct { 613 | // StatusCode is a response status code 614 | StatusCode int 615 | 616 | // IsValidResponse represents a fact of whether a response is treated as valid. You can read more about it in 617 | // HTTPCaller annotation. 618 | IsValidResponse bool 619 | } 620 | -------------------------------------------------------------------------------- /ping_test.go: -------------------------------------------------------------------------------- 1 | package probing 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "net" 8 | "runtime" 9 | "runtime/debug" 10 | "sync" 11 | "sync/atomic" 12 | "testing" 13 | "time" 14 | 15 | "github.com/google/uuid" 16 | "golang.org/x/net/icmp" 17 | "golang.org/x/net/ipv4" 18 | ) 19 | 20 | var testAddr net.Addr = &net.IPAddr{ 21 | IP: net.IPv4(127, 0, 0, 1), 22 | } 23 | 24 | func TestProcessPacket(t *testing.T) { 25 | pinger := makeTestPinger() 26 | shouldBe1 := 0 27 | // this function should be called 28 | pinger.OnRecv = func(pkt *Packet) { 29 | shouldBe1++ 30 | } 31 | 32 | currentUUID := pinger.getCurrentTrackerUUID() 33 | uuidEncoded, err := currentUUID.MarshalBinary() 34 | if err != nil { 35 | t.Fatalf("unable to marshal UUID binary: %s", err) 36 | } 37 | data := append(timeToBytes(time.Now()), uuidEncoded...) 38 | if remainSize := pinger.Size - timeSliceLength - trackerLength; remainSize > 0 { 39 | data = append(data, bytes.Repeat([]byte{1}, remainSize)...) 40 | } 41 | 42 | body := &icmp.Echo{ 43 | ID: pinger.id, 44 | Seq: pinger.sequence, 45 | Data: data, 46 | } 47 | pinger.awaitingSequences[currentUUID][pinger.sequence] = struct{}{} 48 | 49 | msg := &icmp.Message{ 50 | Type: ipv4.ICMPTypeEchoReply, 51 | Code: 0, 52 | Body: body, 53 | } 54 | 55 | msgBytes, _ := msg.Marshal(nil) 56 | 57 | pkt := makeTestPacket(msgBytes) 58 | 59 | err = pinger.processPacket(&pkt) 60 | AssertNoError(t, err) 61 | AssertTrue(t, shouldBe1 == 1) 62 | } 63 | 64 | func TestProcessPacket_IgnoreNonEchoReplies(t *testing.T) { 65 | pinger := makeTestPinger() 66 | shouldBe0 := 0 67 | // this function should not be called because the tracker is mismatched 68 | pinger.OnRecv = func(pkt *Packet) { 69 | shouldBe0++ 70 | } 71 | 72 | currentUUID, err := pinger.getCurrentTrackerUUID().MarshalBinary() 73 | if err != nil { 74 | t.Fatalf("unable to marshal UUID binary: %s", err) 75 | } 76 | data := append(timeToBytes(time.Now()), currentUUID...) 77 | if remainSize := pinger.Size - timeSliceLength - trackerLength; remainSize > 0 { 78 | data = append(data, bytes.Repeat([]byte{1}, remainSize)...) 79 | } 80 | 81 | body := &icmp.Echo{ 82 | ID: pinger.id, 83 | Seq: pinger.sequence, 84 | Data: data, 85 | } 86 | 87 | msg := &icmp.Message{ 88 | Type: ipv4.ICMPTypeDestinationUnreachable, 89 | Code: 0, 90 | Body: body, 91 | } 92 | 93 | msgBytes, _ := msg.Marshal(nil) 94 | 95 | pkt := makeTestPacket(msgBytes) 96 | 97 | err = pinger.processPacket(&pkt) 98 | AssertNoError(t, err) 99 | AssertTrue(t, shouldBe0 == 0) 100 | } 101 | 102 | func TestProcessPacket_IDMismatch(t *testing.T) { 103 | pinger := makeTestPinger() 104 | pinger.protocol = "icmp" // ID is only checked on "icmp" protocol 105 | shouldBe0 := 0 106 | // this function should not be called because the tracker is mismatched 107 | pinger.OnRecv = func(pkt *Packet) { 108 | shouldBe0++ 109 | } 110 | 111 | currentUUID, err := pinger.getCurrentTrackerUUID().MarshalBinary() 112 | if err != nil { 113 | t.Fatalf("unable to marshal UUID binary: %s", err) 114 | } 115 | data := append(timeToBytes(time.Now()), currentUUID...) 116 | if remainSize := pinger.Size - timeSliceLength - trackerLength; remainSize > 0 { 117 | data = append(data, bytes.Repeat([]byte{1}, remainSize)...) 118 | } 119 | 120 | body := &icmp.Echo{ 121 | ID: 999999, 122 | Seq: pinger.sequence, 123 | Data: data, 124 | } 125 | 126 | msg := &icmp.Message{ 127 | Type: ipv4.ICMPTypeEchoReply, 128 | Code: 0, 129 | Body: body, 130 | } 131 | 132 | msgBytes, _ := msg.Marshal(nil) 133 | 134 | pkt := makeTestPacket(msgBytes) 135 | 136 | err = pinger.processPacket(&pkt) 137 | AssertNoError(t, err) 138 | AssertTrue(t, shouldBe0 == 0) 139 | } 140 | 141 | func TestProcessPacket_TrackerMismatch(t *testing.T) { 142 | pinger := makeTestPinger() 143 | shouldBe0 := 0 144 | // this function should not be called because the tracker is mismatched 145 | pinger.OnRecv = func(pkt *Packet) { 146 | shouldBe0++ 147 | } 148 | 149 | testUUID, err := uuid.New().MarshalBinary() 150 | if err != nil { 151 | t.Fatalf("unable to marshal UUID binary: %s", err) 152 | } 153 | data := append(timeToBytes(time.Now()), testUUID...) 154 | if remainSize := pinger.Size - timeSliceLength - trackerLength; remainSize > 0 { 155 | data = append(data, bytes.Repeat([]byte{1}, remainSize)...) 156 | } 157 | 158 | body := &icmp.Echo{ 159 | ID: pinger.id, 160 | Seq: pinger.sequence, 161 | Data: data, 162 | } 163 | 164 | msg := &icmp.Message{ 165 | Type: ipv4.ICMPTypeEchoReply, 166 | Code: 0, 167 | Body: body, 168 | } 169 | 170 | msgBytes, _ := msg.Marshal(nil) 171 | 172 | pkt := makeTestPacket(msgBytes) 173 | 174 | err = pinger.processPacket(&pkt) 175 | AssertNoError(t, err) 176 | AssertTrue(t, shouldBe0 == 0) 177 | } 178 | 179 | func TestProcessPacket_LargePacket(t *testing.T) { 180 | pinger := makeTestPinger() 181 | pinger.Size = 4096 182 | 183 | currentUUID, err := pinger.getCurrentTrackerUUID().MarshalBinary() 184 | if err != nil { 185 | t.Fatalf("unable to marshal UUID binary: %s", err) 186 | } 187 | data := append(timeToBytes(time.Now()), currentUUID...) 188 | if remainSize := pinger.Size - timeSliceLength - trackerLength; remainSize > 0 { 189 | data = append(data, bytes.Repeat([]byte{1}, remainSize)...) 190 | } 191 | 192 | body := &icmp.Echo{ 193 | ID: pinger.id, 194 | Seq: pinger.sequence, 195 | Data: data, 196 | } 197 | 198 | msg := &icmp.Message{ 199 | Type: ipv4.ICMPTypeEchoReply, 200 | Code: 0, 201 | Body: body, 202 | } 203 | 204 | msgBytes, _ := msg.Marshal(nil) 205 | 206 | pkt := makeTestPacket(msgBytes) 207 | 208 | err = pinger.processPacket(&pkt) 209 | AssertNoError(t, err) 210 | } 211 | 212 | func TestProcessPacket_PacketTooSmall(t *testing.T) { 213 | pinger := makeTestPinger() 214 | data := []byte("foo") 215 | 216 | body := &icmp.Echo{ 217 | ID: pinger.id, 218 | Seq: pinger.sequence, 219 | Data: data, 220 | } 221 | 222 | msg := &icmp.Message{ 223 | Type: ipv4.ICMPTypeEchoReply, 224 | Code: 0, 225 | Body: body, 226 | } 227 | 228 | msgBytes, _ := msg.Marshal(nil) 229 | 230 | pkt := makeTestPacket(msgBytes) 231 | 232 | err := pinger.processPacket(&pkt) 233 | AssertError(t, err, "") 234 | } 235 | 236 | func TestNewPingerValid(t *testing.T) { 237 | p := New("www.google.com") 238 | err := p.Resolve() 239 | AssertNoError(t, err) 240 | AssertEqualStrings(t, "www.google.com", p.Addr()) 241 | // DNS names should resolve into IP addresses 242 | AssertNotEqualStrings(t, "www.google.com", p.IPAddr().String()) 243 | AssertTrue(t, isIPv4(p.IPAddr().IP)) 244 | AssertFalse(t, p.Privileged()) 245 | AssertEquals(t, 0, p.tclass) 246 | // Test that SetPrivileged works 247 | p.SetPrivileged(true) 248 | AssertTrue(t, p.Privileged()) 249 | // Test setting to ipv4 address 250 | err = p.SetAddr("www.google.com") 251 | AssertNoError(t, err) 252 | AssertTrue(t, isIPv4(p.IPAddr().IP)) 253 | // Test setting to ipv6 address 254 | err = p.SetAddr("ipv6.google.com") 255 | AssertNoError(t, err) 256 | AssertFalse(t, isIPv4(p.IPAddr().IP)) 257 | // Test setting traffic class 258 | p.SetTrafficClass(192) 259 | AssertEquals(t, 192, p.tclass) 260 | 261 | p = New("localhost") 262 | err = p.Resolve() 263 | AssertNoError(t, err) 264 | AssertEqualStrings(t, "localhost", p.Addr()) 265 | // DNS names should resolve into IP addresses 266 | AssertNotEqualStrings(t, "localhost", p.IPAddr().String()) 267 | AssertTrue(t, isIPv4(p.IPAddr().IP)) 268 | AssertFalse(t, p.Privileged()) 269 | // Test that SetPrivileged works 270 | p.SetPrivileged(true) 271 | AssertTrue(t, p.Privileged()) 272 | // Test setting to ipv4 address 273 | err = p.SetAddr("www.google.com") 274 | AssertNoError(t, err) 275 | AssertTrue(t, isIPv4(p.IPAddr().IP)) 276 | // Test setting to ipv6 address 277 | err = p.SetAddr("ipv6.google.com") 278 | AssertNoError(t, err) 279 | AssertFalse(t, isIPv4(p.IPAddr().IP)) 280 | 281 | p = New("127.0.0.1") 282 | err = p.Resolve() 283 | AssertNoError(t, err) 284 | AssertEqualStrings(t, "127.0.0.1", p.Addr()) 285 | AssertTrue(t, isIPv4(p.IPAddr().IP)) 286 | AssertFalse(t, p.Privileged()) 287 | // Test that SetPrivileged works 288 | p.SetPrivileged(true) 289 | AssertTrue(t, p.Privileged()) 290 | // Test setting to ipv4 address 291 | err = p.SetAddr("www.google.com") 292 | AssertNoError(t, err) 293 | AssertTrue(t, isIPv4(p.IPAddr().IP)) 294 | // Test setting to ipv6 address 295 | err = p.SetAddr("ipv6.google.com") 296 | AssertNoError(t, err) 297 | AssertFalse(t, isIPv4(p.IPAddr().IP)) 298 | 299 | p = New("ipv6.google.com") 300 | err = p.Resolve() 301 | AssertNoError(t, err) 302 | AssertEqualStrings(t, "ipv6.google.com", p.Addr()) 303 | // DNS names should resolve into IP addresses 304 | AssertNotEqualStrings(t, "ipv6.google.com", p.IPAddr().String()) 305 | AssertFalse(t, isIPv4(p.IPAddr().IP)) 306 | AssertFalse(t, p.Privileged()) 307 | // Test that SetPrivileged works 308 | p.SetPrivileged(true) 309 | AssertTrue(t, p.Privileged()) 310 | // Test setting to ipv4 address 311 | err = p.SetAddr("www.google.com") 312 | AssertNoError(t, err) 313 | AssertTrue(t, isIPv4(p.IPAddr().IP)) 314 | // Test setting to ipv6 address 315 | err = p.SetAddr("ipv6.google.com") 316 | AssertNoError(t, err) 317 | AssertFalse(t, isIPv4(p.IPAddr().IP)) 318 | 319 | // ipv6 localhost: 320 | p = New("::1") 321 | err = p.Resolve() 322 | AssertNoError(t, err) 323 | AssertEqualStrings(t, "::1", p.Addr()) 324 | AssertFalse(t, isIPv4(p.IPAddr().IP)) 325 | AssertFalse(t, p.Privileged()) 326 | // Test that SetPrivileged works 327 | p.SetPrivileged(true) 328 | AssertTrue(t, p.Privileged()) 329 | // Test setting to ipv4 address 330 | err = p.SetAddr("www.google.com") 331 | AssertNoError(t, err) 332 | AssertTrue(t, isIPv4(p.IPAddr().IP)) 333 | // Test setting to ipv6 address 334 | err = p.SetAddr("ipv6.google.com") 335 | AssertNoError(t, err) 336 | AssertFalse(t, isIPv4(p.IPAddr().IP)) 337 | } 338 | 339 | func TestNewPingerInvalid(t *testing.T) { 340 | _, err := NewPinger("127.0.0.0.1") 341 | AssertError(t, err, "127.0.0.0.1") 342 | 343 | _, err = NewPinger("127..0.0.1") 344 | AssertError(t, err, "127..0.0.1") 345 | 346 | // The .invalid tld is guaranteed not to exist by RFC2606. 347 | _, err = NewPinger("wtf.invalid.") 348 | AssertError(t, err, "wtf.invalid.") 349 | 350 | _, err = NewPinger(":::1") 351 | AssertError(t, err, ":::1") 352 | 353 | _, err = NewPinger("ipv5.google.com") 354 | AssertError(t, err, "ipv5.google.com") 355 | } 356 | 357 | func TestSetIPAddr(t *testing.T) { 358 | googleaddr, err := net.ResolveIPAddr("ip", "www.google.com") 359 | if err != nil { 360 | t.Fatal("Can't resolve www.google.com, can't run tests") 361 | } 362 | 363 | // Create a localhost ipv4 pinger 364 | p := New("localhost") 365 | err = p.Resolve() 366 | AssertNoError(t, err) 367 | AssertEqualStrings(t, "localhost", p.Addr()) 368 | 369 | // set IPAddr to google 370 | p.SetIPAddr(googleaddr) 371 | AssertEqualStrings(t, googleaddr.String(), p.Addr()) 372 | } 373 | 374 | func TestEmptyIPAddr(t *testing.T) { 375 | _, err := NewPinger("") 376 | AssertError(t, err, "empty pinger did not return an error") 377 | } 378 | 379 | func TestStatisticsSunny(t *testing.T) { 380 | // Create a localhost ipv4 pinger 381 | p := New("localhost") 382 | err := p.Resolve() 383 | AssertNoError(t, err) 384 | AssertEqualStrings(t, "localhost", p.Addr()) 385 | 386 | p.PacketsSent = 10 387 | p.updateStatistics(&Packet{Rtt: time.Duration(1000)}) 388 | p.updateStatistics(&Packet{Rtt: time.Duration(1000)}) 389 | p.updateStatistics(&Packet{Rtt: time.Duration(1000)}) 390 | p.updateStatistics(&Packet{Rtt: time.Duration(1000)}) 391 | p.updateStatistics(&Packet{Rtt: time.Duration(1000)}) 392 | p.updateStatistics(&Packet{Rtt: time.Duration(1000)}) 393 | p.updateStatistics(&Packet{Rtt: time.Duration(1000)}) 394 | p.updateStatistics(&Packet{Rtt: time.Duration(1000)}) 395 | p.updateStatistics(&Packet{Rtt: time.Duration(1000)}) 396 | p.updateStatistics(&Packet{Rtt: time.Duration(1000)}) 397 | 398 | stats := p.Statistics() 399 | if stats.PacketsRecv != 10 { 400 | t.Errorf("Expected %v, got %v", 10, stats.PacketsRecv) 401 | } 402 | if stats.PacketsSent != 10 { 403 | t.Errorf("Expected %v, got %v", 10, stats.PacketsSent) 404 | } 405 | if stats.PacketLoss != 0 { 406 | t.Errorf("Expected %v, got %v", 0, stats.PacketLoss) 407 | } 408 | if stats.MinRtt != time.Duration(1000) { 409 | t.Errorf("Expected %v, got %v", time.Duration(1000), stats.MinRtt) 410 | } 411 | if stats.MaxRtt != time.Duration(1000) { 412 | t.Errorf("Expected %v, got %v", time.Duration(1000), stats.MaxRtt) 413 | } 414 | if stats.AvgRtt != time.Duration(1000) { 415 | t.Errorf("Expected %v, got %v", time.Duration(1000), stats.AvgRtt) 416 | } 417 | if stats.StdDevRtt != time.Duration(0) { 418 | t.Errorf("Expected %v, got %v", time.Duration(0), stats.StdDevRtt) 419 | } 420 | } 421 | 422 | func TestStatisticsLossy(t *testing.T) { 423 | // Create a localhost ipv4 pinger 424 | p := New("localhost") 425 | err := p.Resolve() 426 | AssertNoError(t, err) 427 | AssertEqualStrings(t, "localhost", p.Addr()) 428 | 429 | p.PacketsSent = 20 430 | p.updateStatistics(&Packet{Rtt: time.Duration(10)}) 431 | p.updateStatistics(&Packet{Rtt: time.Duration(1000)}) 432 | p.updateStatistics(&Packet{Rtt: time.Duration(1000)}) 433 | p.updateStatistics(&Packet{Rtt: time.Duration(10000)}) 434 | p.updateStatistics(&Packet{Rtt: time.Duration(1000)}) 435 | p.updateStatistics(&Packet{Rtt: time.Duration(800)}) 436 | p.updateStatistics(&Packet{Rtt: time.Duration(1000)}) 437 | p.updateStatistics(&Packet{Rtt: time.Duration(40)}) 438 | p.updateStatistics(&Packet{Rtt: time.Duration(100000)}) 439 | p.updateStatistics(&Packet{Rtt: time.Duration(1000)}) 440 | 441 | stats := p.Statistics() 442 | if stats.PacketsRecv != 10 { 443 | t.Errorf("Expected %v, got %v", 10, stats.PacketsRecv) 444 | } 445 | if stats.PacketsSent != 20 { 446 | t.Errorf("Expected %v, got %v", 20, stats.PacketsSent) 447 | } 448 | if stats.PacketLoss != 50 { 449 | t.Errorf("Expected %v, got %v", 50, stats.PacketLoss) 450 | } 451 | if stats.MinRtt != time.Duration(10) { 452 | t.Errorf("Expected %v, got %v", time.Duration(10), stats.MinRtt) 453 | } 454 | if stats.MaxRtt != time.Duration(100000) { 455 | t.Errorf("Expected %v, got %v", time.Duration(100000), stats.MaxRtt) 456 | } 457 | if stats.AvgRtt != time.Duration(11585) { 458 | t.Errorf("Expected %v, got %v", time.Duration(11585), stats.AvgRtt) 459 | } 460 | if stats.StdDevRtt != time.Duration(29603) { 461 | t.Errorf("Expected %v, got %v", time.Duration(29603), stats.StdDevRtt) 462 | } 463 | } 464 | 465 | func TestStatisticsZeroDivision(t *testing.T) { 466 | p := New("localhost") 467 | err := p.Resolve() 468 | AssertNoError(t, err) 469 | AssertEqualStrings(t, "localhost", p.Addr()) 470 | 471 | p.PacketsSent = 0 472 | stats := p.Statistics() 473 | 474 | // If packets were not sent (due to send errors), ensure that 475 | // PacketLoss is 0 instead of NaN due to zero division 476 | if stats.PacketLoss != 0 { 477 | t.Errorf("Expected %v, got %v", 0, stats.PacketLoss) 478 | } 479 | } 480 | 481 | func TestSetInterfaceName(t *testing.T) { 482 | pinger := New("localhost") 483 | pinger.Count = 1 484 | pinger.Timeout = time.Second 485 | 486 | // Set loopback interface 487 | pinger.InterfaceName = "lo" 488 | err := pinger.Run() 489 | if runtime.GOOS == "linux" { 490 | AssertNoError(t, err) 491 | } else { 492 | AssertError(t, err, "other platforms unsupport this feature") 493 | } 494 | 495 | // Set fake interface 496 | pinger.InterfaceName = "L()0pB@cK" 497 | err = pinger.Run() 498 | AssertError(t, err, "device not found") 499 | } 500 | 501 | func TestSetSource(t *testing.T) { 502 | pinger := New("localhost") 503 | pinger.Count = 1 504 | pinger.Timeout = time.Second 505 | 506 | // Set source address 507 | pinger.Source = "127.0.0.1" 508 | err := pinger.Run() 509 | AssertNoError(t, err) 510 | 511 | // Set source to invalid IP 512 | pinger.Source = "invalid-ip" 513 | err = pinger.Run() 514 | AssertError(t, err, "invalid source address: invalid-ip") 515 | } 516 | 517 | // Test helpers 518 | func makeTestPinger() *Pinger { 519 | pinger := New("127.0.0.1") 520 | 521 | pinger.ipv4 = true 522 | pinger.addr = "127.0.0.1" 523 | pinger.protocol = "icmp" 524 | pinger.id = 123 525 | pinger.Size = 0 526 | 527 | return pinger 528 | } 529 | 530 | // makeTestPacket emulates a packet with the message msg which come from testAddr 531 | func makeTestPacket(msg []byte) packet { 532 | return packet{ 533 | bytes: msg, 534 | nbytes: len(msg), 535 | ttl: 24, 536 | addr: testAddr, 537 | } 538 | } 539 | 540 | func AssertNoError(t *testing.T, err error) { 541 | t.Helper() 542 | if err != nil { 543 | t.Errorf("Expected No Error but got %s, Stack:\n%s", 544 | err, string(debug.Stack())) 545 | } 546 | } 547 | 548 | func AssertError(t *testing.T, err error, info string) { 549 | t.Helper() 550 | if err == nil { 551 | t.Errorf("Expected Error but got %s, %s, Stack:\n%s", 552 | err, info, string(debug.Stack())) 553 | } 554 | } 555 | 556 | func AssertEqualStrings(t *testing.T, expected, actual string) { 557 | t.Helper() 558 | if expected != actual { 559 | t.Errorf("Expected %s, got %s, Stack:\n%s", 560 | expected, actual, string(debug.Stack())) 561 | } 562 | } 563 | 564 | func AssertEquals[T comparable](t *testing.T, expected, actual T) { 565 | t.Helper() 566 | if expected != actual { 567 | t.Errorf("Expected %v, got %v, Stack:\n%s", 568 | expected, actual, string(debug.Stack())) 569 | } 570 | } 571 | 572 | func AssertNotEqualStrings(t *testing.T, expected, actual string) { 573 | t.Helper() 574 | if expected == actual { 575 | t.Errorf("Expected %s, got %s, Stack:\n%s", 576 | expected, actual, string(debug.Stack())) 577 | } 578 | } 579 | 580 | func AssertTrue(t *testing.T, b bool) { 581 | t.Helper() 582 | if !b { 583 | t.Errorf("Expected True, got False, Stack:\n%s", string(debug.Stack())) 584 | } 585 | } 586 | 587 | func AssertFalse(t *testing.T, b bool) { 588 | t.Helper() 589 | if b { 590 | t.Errorf("Expected False, got True, Stack:\n%s", string(debug.Stack())) 591 | } 592 | } 593 | 594 | func BenchmarkProcessPacket(b *testing.B) { 595 | pinger := New("127.0.0.1") 596 | 597 | pinger.ipv4 = true 598 | pinger.addr = "127.0.0.1" 599 | pinger.protocol = "ip4:icmp" 600 | pinger.id = 123 601 | 602 | currentUUID, err := pinger.getCurrentTrackerUUID().MarshalBinary() 603 | if err != nil { 604 | b.Fatalf("unable to marshal UUID binary: %s", err) 605 | } 606 | data := append(timeToBytes(time.Now()), currentUUID...) 607 | if remainSize := pinger.Size - timeSliceLength - trackerLength; remainSize > 0 { 608 | data = append(data, bytes.Repeat([]byte{1}, remainSize)...) 609 | } 610 | 611 | body := &icmp.Echo{ 612 | ID: pinger.id, 613 | Seq: pinger.sequence, 614 | Data: data, 615 | } 616 | 617 | msg := &icmp.Message{ 618 | Type: ipv4.ICMPTypeEchoReply, 619 | Code: 0, 620 | Body: body, 621 | } 622 | 623 | msgBytes, _ := msg.Marshal(nil) 624 | 625 | pkt := makeTestPacket(msgBytes) 626 | 627 | for k := 0; k < b.N; k++ { 628 | pinger.processPacket(&pkt) 629 | } 630 | } 631 | 632 | func TestProcessPacket_IgnoresDuplicateSequence(t *testing.T) { 633 | pinger := makeTestPinger() 634 | // pinger.protocol = "icmp" // ID is only checked on "icmp" protocol 635 | shouldBe0 := 0 636 | dups := 0 637 | 638 | // this function should not be called because the tracker is mismatched 639 | pinger.OnRecv = func(pkt *Packet) { 640 | shouldBe0++ 641 | } 642 | 643 | pinger.OnDuplicateRecv = func(pkt *Packet) { 644 | dups++ 645 | } 646 | 647 | currentUUID := pinger.getCurrentTrackerUUID() 648 | uuidEncoded, err := currentUUID.MarshalBinary() 649 | if err != nil { 650 | t.Fatalf("unable to marshal UUID binary: %s", err) 651 | } 652 | data := append(timeToBytes(time.Now()), uuidEncoded...) 653 | if remainSize := pinger.Size - timeSliceLength - trackerLength; remainSize > 0 { 654 | data = append(data, bytes.Repeat([]byte{1}, remainSize)...) 655 | } 656 | 657 | body := &icmp.Echo{ 658 | ID: 123, 659 | Seq: 0, 660 | Data: data, 661 | } 662 | // register the sequence as sent 663 | pinger.awaitingSequences[currentUUID][0] = struct{}{} 664 | 665 | msg := &icmp.Message{ 666 | Type: ipv4.ICMPTypeEchoReply, 667 | Code: 0, 668 | Body: body, 669 | } 670 | 671 | msgBytes, _ := msg.Marshal(nil) 672 | 673 | pkt := makeTestPacket(msgBytes) 674 | 675 | err = pinger.processPacket(&pkt) 676 | AssertNoError(t, err) 677 | // receive a duplicate 678 | err = pinger.processPacket(&pkt) 679 | AssertNoError(t, err) 680 | 681 | AssertTrue(t, shouldBe0 == 1) 682 | AssertTrue(t, dups == 1) 683 | AssertTrue(t, pinger.PacketsRecvDuplicates == 1) 684 | } 685 | 686 | type testPacketConn struct{} 687 | 688 | func (c testPacketConn) Close() error { return nil } 689 | func (c testPacketConn) ICMPRequestType() icmp.Type { return ipv4.ICMPTypeEcho } 690 | func (c testPacketConn) SetFlagTTL() error { return nil } 691 | func (c testPacketConn) SetReadDeadline(t time.Time) error { return nil } 692 | func (c testPacketConn) SetTTL(t int) {} 693 | func (c testPacketConn) SetMark(m uint) error { return nil } 694 | func (c testPacketConn) SetDoNotFragment() error { return nil } 695 | func (c testPacketConn) SetBroadcastFlag() error { return nil } 696 | func (c testPacketConn) InstallICMPIDFilter(id int) error { return nil } 697 | func (c testPacketConn) SetIfIndex(ifIndex int) {} 698 | func (c testPacketConn) SetSource(source net.IP) {} 699 | func (c testPacketConn) SetTrafficClass(uint8) error { return nil } 700 | 701 | func (c testPacketConn) ReadFrom(b []byte) (n int, ttl int, src net.Addr, err error) { 702 | return 0, 0, testAddr, nil 703 | } 704 | 705 | func (c testPacketConn) WriteTo(b []byte, dst net.Addr) (int, error) { 706 | return len(b), nil 707 | } 708 | 709 | type testPacketConnBadWrite struct { 710 | testPacketConn 711 | } 712 | 713 | func (c testPacketConnBadWrite) WriteTo(b []byte, dst net.Addr) (int, error) { 714 | return 0, errors.New("bad write") 715 | } 716 | 717 | func TestRunBadWrite(t *testing.T) { 718 | pinger := New("127.0.0.1") 719 | pinger.Count = 1 720 | 721 | err := pinger.Resolve() 722 | AssertNoError(t, err) 723 | 724 | var conn testPacketConnBadWrite 725 | 726 | err = pinger.run(context.Background(), conn) 727 | AssertTrue(t, err != nil) 728 | 729 | stats := pinger.Statistics() 730 | AssertTrue(t, stats != nil) 731 | if stats == nil { 732 | t.FailNow() 733 | } 734 | AssertTrue(t, stats.PacketsSent == 0) 735 | AssertTrue(t, stats.PacketsRecv == 0) 736 | } 737 | 738 | type testPacketConnBadRead struct { 739 | testPacketConn 740 | } 741 | 742 | func (c testPacketConnBadRead) ReadFrom(b []byte) (n int, ttl int, src net.Addr, err error) { 743 | return 0, 0, testAddr, errors.New("bad read") 744 | } 745 | 746 | func TestRunBadRead(t *testing.T) { 747 | pinger := New("127.0.0.1") 748 | pinger.Count = 1 749 | 750 | err := pinger.Resolve() 751 | AssertNoError(t, err) 752 | 753 | var conn testPacketConnBadRead 754 | 755 | err = pinger.run(context.Background(), conn) 756 | AssertTrue(t, err != nil) 757 | 758 | stats := pinger.Statistics() 759 | AssertTrue(t, stats != nil) 760 | if stats == nil { 761 | t.FailNow() 762 | } 763 | AssertTrue(t, stats.PacketsSent == 1) 764 | AssertTrue(t, stats.PacketsRecv == 0) 765 | } 766 | 767 | type testPacketConnOK struct { 768 | testPacketConn 769 | m sync.Mutex 770 | writeDone int32 771 | buf []byte 772 | dst net.Addr 773 | } 774 | 775 | func (c *testPacketConnOK) WriteTo(b []byte, dst net.Addr) (int, error) { 776 | c.m.Lock() 777 | defer c.m.Unlock() 778 | c.buf = make([]byte, len(b)) 779 | c.dst = dst 780 | n := copy(c.buf, b) 781 | atomic.StoreInt32(&c.writeDone, 1) 782 | return n, nil 783 | } 784 | 785 | func (c *testPacketConnOK) ReadFrom(b []byte) (n int, ttl int, src net.Addr, err error) { 786 | c.m.Lock() 787 | defer c.m.Unlock() 788 | if atomic.LoadInt32(&c.writeDone) == 0 { 789 | return 0, 0, testAddr, nil 790 | } 791 | msg, err := icmp.ParseMessage(ipv4.ICMPTypeEcho.Protocol(), c.buf) 792 | if err != nil { 793 | return 0, 0, nil, err 794 | } 795 | msg.Type = ipv4.ICMPTypeEchoReply 796 | buf, err := msg.Marshal(nil) 797 | if err != nil { 798 | return 0, 0, nil, err 799 | } 800 | time.Sleep(10 * time.Millisecond) 801 | return copy(b, buf), 64, testAddr, nil 802 | } 803 | 804 | func TestRunOK(t *testing.T) { 805 | pinger := New("127.0.0.1") 806 | pinger.Count = 1 807 | 808 | err := pinger.Resolve() 809 | AssertNoError(t, err) 810 | 811 | conn := new(testPacketConnOK) 812 | 813 | err = pinger.run(context.Background(), conn) 814 | AssertTrue(t, err == nil) 815 | 816 | stats := pinger.Statistics() 817 | AssertTrue(t, stats != nil) 818 | if stats == nil { 819 | t.FailNow() 820 | } 821 | AssertTrue(t, stats.PacketsSent == 1) 822 | AssertTrue(t, stats.PacketsRecv == 1) 823 | AssertTrue(t, stats.MinRtt >= 10*time.Millisecond) 824 | AssertTrue(t, stats.MinRtt <= 12*time.Millisecond) 825 | } 826 | 827 | func TestRunWithTimeoutContext(t *testing.T) { 828 | pinger := New("127.0.0.1") 829 | 830 | err := pinger.Resolve() 831 | AssertNoError(t, err) 832 | 833 | conn := new(testPacketConnOK) 834 | 835 | start := time.Now() 836 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 837 | defer cancel() 838 | err = pinger.run(ctx, conn) 839 | AssertTrue(t, errors.Is(err, context.DeadlineExceeded)) 840 | elapsedTime := time.Since(start) 841 | AssertTrue(t, elapsedTime < 10*time.Second) 842 | 843 | stats := pinger.Statistics() 844 | AssertTrue(t, stats != nil) 845 | if stats == nil { 846 | t.FailNow() 847 | } 848 | AssertTrue(t, stats.PacketsSent > 0) 849 | AssertTrue(t, stats.PacketsRecv > 0) 850 | } 851 | 852 | func TestRunWithBackgroundContext(t *testing.T) { 853 | pinger := New("127.0.0.1") 854 | pinger.Count = 10 855 | pinger.Interval = 100 * time.Millisecond 856 | 857 | err := pinger.Resolve() 858 | AssertNoError(t, err) 859 | 860 | conn := new(testPacketConnOK) 861 | 862 | err = pinger.run(context.Background(), conn) 863 | AssertTrue(t, err == nil) 864 | 865 | stats := pinger.Statistics() 866 | AssertTrue(t, stats != nil) 867 | if stats == nil { 868 | t.FailNow() 869 | } 870 | AssertTrue(t, stats.PacketsRecv == 10) 871 | } 872 | 873 | func TestSetResolveTimeout(t *testing.T) { 874 | p := New("www.google.com") 875 | p.Count = 3 876 | p.Timeout = 5 * time.Second 877 | p.ResolveTimeout = 2 * time.Second 878 | err := p.Resolve() 879 | AssertNoError(t, err) 880 | 881 | err = p.SetAddr("www.google.com ") 882 | AssertError(t, err, "") 883 | 884 | err = p.SetAddr("127.0.0.1 ") 885 | AssertError(t, err, "") 886 | 887 | err = p.SetAddr("127.0.0.1") 888 | AssertNoError(t, err) 889 | } 890 | 891 | func TestRunStatisticsConcurrent(t *testing.T) { 892 | p := New("www.google.com") 893 | p.Count = 1 894 | p.Interval = time.Millisecond 895 | go p.Statistics() 896 | p.Run() 897 | } 898 | -------------------------------------------------------------------------------- /ping.go: -------------------------------------------------------------------------------- 1 | // Package probing is a simple but powerful ICMP echo (ping) library. 2 | // 3 | // Here is a very simple example that sends and receives three packets: 4 | // 5 | // pinger, err := probing.NewPinger("www.google.com") 6 | // if err != nil { 7 | // panic(err) 8 | // } 9 | // pinger.Count = 3 10 | // err = pinger.Run() // blocks until finished 11 | // if err != nil { 12 | // panic(err) 13 | // } 14 | // stats := pinger.Statistics() // get send/receive/rtt stats 15 | // 16 | // Here is an example that emulates the traditional UNIX ping command: 17 | // 18 | // pinger, err := probing.NewPinger("www.google.com") 19 | // if err != nil { 20 | // panic(err) 21 | // } 22 | // // Listen for Ctrl-C. 23 | // c := make(chan os.Signal, 1) 24 | // signal.Notify(c, os.Interrupt) 25 | // go func() { 26 | // for _ = range c { 27 | // pinger.Stop() 28 | // } 29 | // }() 30 | // pinger.OnRecv = func(pkt *probing.Packet) { 31 | // fmt.Printf("%d bytes from %s: icmp_seq=%d time=%v\n", 32 | // pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt) 33 | // } 34 | // pinger.OnFinish = func(stats *probing.Statistics) { 35 | // fmt.Printf("\n--- %s ping statistics ---\n", stats.Addr) 36 | // fmt.Printf("%d packets transmitted, %d packets received, %v%% packet loss\n", 37 | // stats.PacketsSent, stats.PacketsRecv, stats.PacketLoss) 38 | // fmt.Printf("round-trip min/avg/max/stddev = %v/%v/%v/%v\n", 39 | // stats.MinRtt, stats.AvgRtt, stats.MaxRtt, stats.StdDevRtt) 40 | // } 41 | // fmt.Printf("PING %s (%s):\n", pinger.Addr(), pinger.IPAddr()) 42 | // err = pinger.Run() 43 | // if err != nil { 44 | // panic(err) 45 | // } 46 | // 47 | // It sends ICMP Echo Request packet(s) and waits for an Echo Reply in response. 48 | // If it receives a response, it calls the OnRecv callback. When it's finished, 49 | // it calls the OnFinish callback. 50 | // 51 | // For a full ping example, see "cmd/ping/ping.go". 52 | package probing 53 | 54 | import ( 55 | "bytes" 56 | "context" 57 | "errors" 58 | "fmt" 59 | "log" 60 | "math" 61 | "math/rand" 62 | "net" 63 | "runtime" 64 | "sync" 65 | "sync/atomic" 66 | "syscall" 67 | "time" 68 | 69 | "github.com/google/uuid" 70 | "golang.org/x/net/icmp" 71 | "golang.org/x/net/ipv4" 72 | "golang.org/x/net/ipv6" 73 | "golang.org/x/sync/errgroup" 74 | ) 75 | 76 | const ( 77 | timeSliceLength = 8 78 | trackerLength = len(uuid.UUID{}) 79 | protocolICMP = 1 80 | protocolIPv6ICMP = 58 81 | 82 | networkIP = "ip" 83 | networkIPv4 = "ip4" 84 | networkIPv6 = "ip6" 85 | ) 86 | 87 | var ( 88 | ipv4Proto = map[string]string{"icmp": "ip4:icmp", "udp": "udp4"} 89 | ipv6Proto = map[string]string{"icmp": "ip6:ipv6-icmp", "udp": "udp6"} 90 | 91 | ErrMarkNotSupported = errors.New("setting SO_MARK socket option is not supported on this platform") 92 | ErrDFNotSupported = errors.New("setting do-not-fragment bit is not supported on this platform") 93 | ) 94 | 95 | // New returns a new Pinger struct pointer. 96 | func New(addr string) *Pinger { 97 | r := rand.New(rand.NewSource(getSeed())) 98 | firstUUID := uuid.New() 99 | var firstSequence = map[uuid.UUID]map[int]struct{}{} 100 | firstSequence[firstUUID] = make(map[int]struct{}) 101 | return &Pinger{ 102 | Count: -1, 103 | Interval: time.Second, 104 | RecordRtts: true, 105 | RecordTTLs: true, 106 | Size: timeSliceLength + trackerLength, 107 | Timeout: time.Duration(math.MaxInt64), 108 | 109 | addr: addr, 110 | done: make(chan interface{}), 111 | id: r.Intn(math.MaxUint16), 112 | trackerUUIDs: []uuid.UUID{firstUUID}, 113 | ipaddr: nil, 114 | ipv4: false, 115 | network: networkIP, 116 | protocol: "udp", 117 | awaitingSequences: firstSequence, 118 | TTL: 64, 119 | tclass: 0, 120 | logger: StdLogger{Logger: log.New(log.Writer(), log.Prefix(), log.Flags())}, 121 | } 122 | } 123 | 124 | // NewPinger returns a new Pinger and resolves the address. 125 | func NewPinger(addr string) (*Pinger, error) { 126 | p := New(addr) 127 | return p, p.Resolve() 128 | } 129 | 130 | // Pinger represents a packet sender/receiver. 131 | type Pinger struct { 132 | // Interval is the wait time between each packet send. Default is 1s. 133 | Interval time.Duration 134 | 135 | // Timeout specifies a timeout before ping exits, regardless of how many 136 | // packets have been received. 137 | Timeout time.Duration 138 | 139 | // ResolveTimeout specifies a timeout to resolve an IP address or domain name 140 | ResolveTimeout time.Duration 141 | 142 | // Count tells pinger to stop after sending (and receiving) Count echo 143 | // packets. If this option is not specified, pinger will operate until 144 | // interrupted. 145 | Count int 146 | 147 | // Debug runs in debug mode 148 | Debug bool 149 | 150 | // Number of packets sent 151 | PacketsSent int 152 | 153 | // Number of packets received 154 | PacketsRecv int 155 | 156 | // Number of duplicate packets received 157 | PacketsRecvDuplicates int 158 | 159 | // Round trip time statistics 160 | minRtt time.Duration 161 | maxRtt time.Duration 162 | avgRtt time.Duration 163 | stdDevRtt time.Duration 164 | stddevm2 float64 165 | statsMu sync.RWMutex 166 | 167 | // If true, keep a record of rtts of all received packets. 168 | // Set to false to avoid memory bloat for long running pings. 169 | RecordRtts bool 170 | 171 | // If true, keep a record of TTLs of all received packets. 172 | // Set to false to avoid memory bloat for long running pings. 173 | RecordTTLs bool 174 | 175 | // rtts is all of the Rtts 176 | rtts []time.Duration 177 | 178 | // ttls is all of the TTLs 179 | ttls []uint8 180 | 181 | // OnSetup is called when Pinger has finished setting up the listening socket 182 | OnSetup func() 183 | 184 | // OnSend is called when Pinger sends a packet 185 | OnSend func(*Packet) 186 | 187 | // OnRecv is called when Pinger receives and processes a packet 188 | OnRecv func(*Packet) 189 | 190 | // OnFinish is called when Pinger exits 191 | OnFinish func(*Statistics) 192 | 193 | // OnDuplicateRecv is called when a packet is received that has already been received. 194 | OnDuplicateRecv func(*Packet) 195 | 196 | // OnSendError is called when an error occurs while Pinger attempts to send a packet 197 | OnSendError func(*Packet, error) 198 | 199 | // OnRecvError is called when an error occurs while Pinger attempts to receive a packet 200 | OnRecvError func(error) 201 | 202 | // Size of packet being sent 203 | Size int 204 | 205 | // Tracker: Used to uniquely identify packets - Deprecated 206 | Tracker uint64 207 | 208 | // Source is the source IP address 209 | Source string 210 | 211 | // Interface used to send/recv ICMP messages 212 | InterfaceName string 213 | 214 | // Channel and mutex used to communicate when the Pinger should stop between goroutines. 215 | done chan interface{} 216 | lock sync.Mutex 217 | 218 | ipaddr *net.IPAddr 219 | addr string 220 | 221 | // mark is a SO_MARK (fwmark) set on outgoing icmp packets 222 | mark uint 223 | 224 | // df when true sets the do-not-fragment bit in the outer IP or IPv6 header 225 | df bool 226 | 227 | // trackerUUIDs is the list of UUIDs being used for sending packets. 228 | trackerUUIDs []uuid.UUID 229 | 230 | ipv4 bool 231 | id int 232 | sequence int 233 | // awaitingSequences are in-flight sequence numbers we keep track of to help remove duplicate receipts 234 | awaitingSequences map[uuid.UUID]map[int]struct{} 235 | // network is one of "ip", "ip4", or "ip6". 236 | network string 237 | // protocol is "icmp" or "udp". 238 | protocol string 239 | 240 | logger Logger 241 | 242 | TTL int 243 | 244 | // tclass defines the traffic class (ToS for IPv4) set on outgoing icmp packets 245 | tclass uint8 246 | } 247 | 248 | type packet struct { 249 | bytes []byte 250 | nbytes int 251 | ttl int 252 | addr net.Addr 253 | } 254 | 255 | // Packet represents a received and processed ICMP echo packet. 256 | type Packet struct { 257 | // Rtt is the round-trip time it took to ping. 258 | Rtt time.Duration 259 | 260 | // IPAddr is the address of the host being pinged. 261 | IPAddr *net.IPAddr 262 | 263 | // Addr is the string address of the host being pinged. 264 | Addr string 265 | 266 | // NBytes is the number of bytes in the message. 267 | Nbytes int 268 | 269 | // Seq is the ICMP sequence number. 270 | Seq int 271 | 272 | // TTL is the Time To Live on the packet. 273 | TTL int 274 | 275 | // ID is the ICMP identifier. 276 | ID int 277 | } 278 | 279 | // Statistics represent the stats of a currently running or finished 280 | // pinger operation. 281 | type Statistics struct { 282 | // PacketsRecv is the number of packets received. 283 | PacketsRecv int 284 | 285 | // PacketsSent is the number of packets sent. 286 | PacketsSent int 287 | 288 | // PacketsRecvDuplicates is the number of duplicate responses there were to a sent packet. 289 | PacketsRecvDuplicates int 290 | 291 | // PacketLoss is the percentage of packets lost. 292 | PacketLoss float64 293 | 294 | // IPAddr is the address of the host being pinged. 295 | IPAddr *net.IPAddr 296 | 297 | // Addr is the string address of the host being pinged. 298 | Addr string 299 | 300 | // Rtts is all of the round-trip times sent via this pinger. 301 | Rtts []time.Duration 302 | 303 | // TTLs is all of the TTLs received via this pinger. 304 | TTLs []uint8 305 | 306 | // MinRtt is the minimum round-trip time sent via this pinger. 307 | MinRtt time.Duration 308 | 309 | // MaxRtt is the maximum round-trip time sent via this pinger. 310 | MaxRtt time.Duration 311 | 312 | // AvgRtt is the average round-trip time sent via this pinger. 313 | AvgRtt time.Duration 314 | 315 | // StdDevRtt is the standard deviation of the round-trip times sent via 316 | // this pinger. 317 | StdDevRtt time.Duration 318 | } 319 | 320 | func (p *Pinger) updateStatistics(pkt *Packet) { 321 | p.statsMu.Lock() 322 | defer p.statsMu.Unlock() 323 | 324 | p.PacketsRecv++ 325 | if p.RecordRtts { 326 | p.rtts = append(p.rtts, pkt.Rtt) 327 | } 328 | 329 | if p.RecordTTLs { 330 | p.ttls = append(p.ttls, uint8(pkt.TTL)) 331 | } 332 | 333 | if p.PacketsRecv == 1 || pkt.Rtt < p.minRtt { 334 | p.minRtt = pkt.Rtt 335 | } 336 | 337 | if pkt.Rtt > p.maxRtt { 338 | p.maxRtt = pkt.Rtt 339 | } 340 | 341 | pktCount := time.Duration(p.PacketsRecv) 342 | // welford's online method for stddev 343 | // https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm 344 | delta := pkt.Rtt - p.avgRtt 345 | p.avgRtt += delta / pktCount 346 | delta2 := pkt.Rtt - p.avgRtt 347 | p.stddevm2 += float64(delta) * float64(delta2) 348 | 349 | p.stdDevRtt = time.Duration(math.Sqrt(p.stddevm2 / float64(pktCount))) 350 | } 351 | 352 | // SetIPAddr sets the ip address of the target host. 353 | func (p *Pinger) SetIPAddr(ipaddr *net.IPAddr) { 354 | p.ipv4 = isIPv4(ipaddr.IP) 355 | 356 | p.statsMu.Lock() 357 | p.ipaddr = ipaddr 358 | p.addr = ipaddr.String() 359 | p.statsMu.Unlock() 360 | } 361 | 362 | // IPAddr returns the ip address of the target host. 363 | func (p *Pinger) IPAddr() *net.IPAddr { 364 | return p.ipaddr 365 | } 366 | 367 | // Resolve does the DNS lookup for the Pinger address and sets IP protocol. 368 | func (p *Pinger) Resolve() error { 369 | if len(p.addr) == 0 { 370 | return errors.New("addr cannot be empty") 371 | } 372 | var ( 373 | ipaddr *net.IPAddr 374 | err error 375 | ) 376 | if p.ResolveTimeout > time.Duration(0) { 377 | var ( 378 | ctx = context.Background() 379 | ips []net.IP 380 | ) 381 | ctx, cancel := context.WithTimeout(ctx, p.ResolveTimeout) 382 | defer cancel() 383 | ips, err = net.DefaultResolver.LookupIP(ctx, p.network, p.addr) 384 | if err != nil { 385 | return err 386 | } 387 | if len(ips) == 0 { 388 | return fmt.Errorf("lookup %s failed: no addresses found", p.addr) 389 | } 390 | ipaddr = &net.IPAddr{IP: ips[0]} 391 | for _, ip := range ips { 392 | if p.network == networkIPv6 { 393 | if ip.To4() == nil && ip.To16() != nil { 394 | ipaddr = &net.IPAddr{IP: ip} 395 | break 396 | } 397 | continue 398 | } 399 | if ip.To4() != nil { 400 | ipaddr = &net.IPAddr{IP: ip} 401 | } 402 | } 403 | } else { 404 | ipaddr, err = net.ResolveIPAddr(p.network, p.addr) 405 | if err != nil { 406 | return err 407 | } 408 | } 409 | 410 | p.ipv4 = isIPv4(ipaddr.IP) 411 | 412 | p.statsMu.Lock() 413 | p.ipaddr = ipaddr 414 | p.statsMu.Unlock() 415 | 416 | return nil 417 | } 418 | 419 | // SetAddr resolves and sets the ip address of the target host, addr can be a 420 | // DNS name like "www.google.com" or IP like "127.0.0.1". 421 | func (p *Pinger) SetAddr(addr string) error { 422 | oldAddr := p.addr 423 | p.statsMu.Lock() 424 | p.addr = addr 425 | p.statsMu.Unlock() 426 | err := p.Resolve() 427 | if err != nil { 428 | p.statsMu.Lock() 429 | p.addr = oldAddr 430 | p.statsMu.Unlock() 431 | return err 432 | } 433 | return nil 434 | } 435 | 436 | // Addr returns the string ip address of the target host. 437 | func (p *Pinger) Addr() string { 438 | return p.addr 439 | } 440 | 441 | // SetNetwork allows configuration of DNS resolution. 442 | // * "ip" will automatically select IPv4 or IPv6. 443 | // * "ip4" will select IPv4. 444 | // * "ip6" will select IPv6. 445 | func (p *Pinger) SetNetwork(n string) { 446 | switch n { 447 | case networkIPv4: 448 | p.network = networkIPv4 449 | case networkIPv6: 450 | p.network = networkIPv6 451 | default: 452 | p.network = networkIP 453 | } 454 | } 455 | 456 | // SetPrivileged sets the type of ping pinger will send. 457 | // false means pinger will send an "unprivileged" UDP ping. 458 | // true means pinger will send a "privileged" raw ICMP ping. 459 | // NOTE: setting to true requires that it be run with super-user privileges. 460 | func (p *Pinger) SetPrivileged(privileged bool) { 461 | if privileged { 462 | p.protocol = "icmp" 463 | } else { 464 | p.protocol = "udp" 465 | } 466 | } 467 | 468 | // Privileged returns whether pinger is running in privileged mode. 469 | func (p *Pinger) Privileged() bool { 470 | return p.protocol == "icmp" 471 | } 472 | 473 | // SetLogger sets the logger to be used to log events from the pinger. 474 | func (p *Pinger) SetLogger(logger Logger) { 475 | p.logger = logger 476 | } 477 | 478 | // SetID sets the ICMP identifier. 479 | func (p *Pinger) SetID(id int) { 480 | p.id = id 481 | } 482 | 483 | // ID returns the ICMP identifier. 484 | func (p *Pinger) ID() int { 485 | return p.id 486 | } 487 | 488 | // SetMark sets a mark intended to be set on outgoing ICMP packets. 489 | func (p *Pinger) SetMark(m uint) { 490 | p.mark = m 491 | } 492 | 493 | // Mark returns the mark to be set on outgoing ICMP packets. 494 | func (p *Pinger) Mark() uint { 495 | return p.mark 496 | } 497 | 498 | // SetDoNotFragment sets the do-not-fragment bit in the outer IP header to the desired value. 499 | func (p *Pinger) SetDoNotFragment(df bool) { 500 | p.df = df 501 | } 502 | 503 | // SetTrafficClass sets the traffic class (type-of-service field for IPv4) field 504 | // value for future outgoing packets. 505 | func (p *Pinger) SetTrafficClass(tc uint8) { 506 | p.tclass = tc 507 | } 508 | 509 | // TrafficClass returns the traffic class field (type-of-service field for IPv4) 510 | // value for outgoing packets. 511 | func (p *Pinger) TrafficClass() uint8 { 512 | return p.tclass 513 | } 514 | 515 | // Run runs the pinger. This is a blocking function that will exit when it's 516 | // done. If Count or Interval are not specified, it will run continuously until 517 | // it is interrupted. 518 | func (p *Pinger) Run() error { 519 | return p.RunWithContext(context.Background()) 520 | } 521 | 522 | // RunWithContext runs the pinger with a context. This is a blocking function that will exit when it's 523 | // done or if the context is canceled. If Count or Interval are not specified, it will run continuously until 524 | // it is interrupted. 525 | func (p *Pinger) RunWithContext(ctx context.Context) error { 526 | var conn packetConn 527 | var err error 528 | if p.Size < timeSliceLength+trackerLength { 529 | return fmt.Errorf("size %d is less than minimum required size %d", p.Size, timeSliceLength+trackerLength) 530 | } 531 | if p.ipaddr == nil { 532 | err = p.Resolve() 533 | } 534 | if err != nil { 535 | return err 536 | } 537 | if conn, err = p.listen(); err != nil { 538 | return err 539 | } 540 | defer conn.Close() 541 | 542 | if p.mark != 0 { 543 | if err := conn.SetMark(p.mark); err != nil { 544 | return fmt.Errorf("error setting mark: %v", err) 545 | } 546 | } 547 | 548 | if p.df { 549 | if err := conn.SetDoNotFragment(); err != nil { 550 | return fmt.Errorf("error setting do-not-fragment: %v", err) 551 | } 552 | } 553 | 554 | if p.tclass != 0 { 555 | if err := conn.SetTrafficClass(p.tclass); err != nil { 556 | return fmt.Errorf("error setting traffic class: %v", err) 557 | } 558 | } 559 | 560 | conn.SetTTL(p.TTL) 561 | if p.InterfaceName != "" { 562 | iface, err := net.InterfaceByName(p.InterfaceName) 563 | if err != nil { 564 | return err 565 | } 566 | conn.SetIfIndex(iface.Index) 567 | } 568 | 569 | if p.Source != "" { 570 | ip := net.ParseIP(p.Source) 571 | if ip == nil { 572 | return fmt.Errorf("invalid source address: %s", p.Source) 573 | } 574 | conn.SetSource(ip) 575 | } 576 | 577 | return p.run(ctx, conn) 578 | } 579 | 580 | func (p *Pinger) run(ctx context.Context, conn packetConn) error { 581 | if err := conn.SetFlagTTL(); err != nil { 582 | return err 583 | } 584 | defer p.finish() 585 | 586 | recv := make(chan *packet, 5) 587 | defer close(recv) 588 | 589 | if p.OnSetup != nil { 590 | p.OnSetup() 591 | } 592 | 593 | g, ctx := errgroup.WithContext(ctx) 594 | 595 | g.Go(func() error { 596 | select { 597 | case <-ctx.Done(): 598 | p.Stop() 599 | return ctx.Err() 600 | case <-p.done: 601 | } 602 | return nil 603 | }) 604 | 605 | g.Go(func() error { 606 | defer p.Stop() 607 | return p.recvICMP(conn, recv) 608 | }) 609 | 610 | g.Go(func() error { 611 | defer p.Stop() 612 | return p.runLoop(conn, recv) 613 | }) 614 | 615 | return g.Wait() 616 | } 617 | 618 | func (p *Pinger) runLoop( 619 | conn packetConn, 620 | recvCh <-chan *packet, 621 | ) error { 622 | logger := p.logger 623 | if logger == nil { 624 | logger = NoopLogger{} 625 | } 626 | 627 | timeout := time.NewTicker(p.Timeout) 628 | interval := time.NewTicker(p.Interval) 629 | defer func() { 630 | interval.Stop() 631 | timeout.Stop() 632 | }() 633 | 634 | if err := p.sendICMP(conn); err != nil { 635 | return err 636 | } 637 | 638 | for { 639 | select { 640 | case <-p.done: 641 | return nil 642 | 643 | case <-timeout.C: 644 | return nil 645 | 646 | case r := <-recvCh: 647 | err := p.processPacket(r) 648 | if err != nil { 649 | // FIXME: this logs as FATAL but continues 650 | logger.Fatalf("processing received packet: %s", err) 651 | } 652 | 653 | case <-interval.C: 654 | if p.Count > 0 && p.PacketsSent >= p.Count { 655 | interval.Stop() 656 | continue 657 | } 658 | err := p.sendICMP(conn) 659 | if err != nil { 660 | // FIXME: this logs as FATAL but continues 661 | logger.Fatalf("sending packet: %s", err) 662 | } 663 | } 664 | if p.Count > 0 && p.PacketsRecv >= p.Count { 665 | return nil 666 | } 667 | } 668 | } 669 | 670 | func (p *Pinger) Stop() { 671 | p.lock.Lock() 672 | defer p.lock.Unlock() 673 | 674 | open := true 675 | select { 676 | case _, open = <-p.done: 677 | default: 678 | } 679 | 680 | if open { 681 | close(p.done) 682 | } 683 | } 684 | 685 | func (p *Pinger) finish() { 686 | if p.OnFinish != nil { 687 | p.OnFinish(p.Statistics()) 688 | } 689 | } 690 | 691 | // Statistics returns the statistics of the pinger. This can be run while the 692 | // pinger is running or after it is finished. OnFinish calls this function to 693 | // get it's finished statistics. 694 | func (p *Pinger) Statistics() *Statistics { 695 | p.statsMu.RLock() 696 | defer p.statsMu.RUnlock() 697 | sent := p.PacketsSent 698 | 699 | var loss float64 700 | if sent > 0 { 701 | loss = float64(sent-p.PacketsRecv) / float64(sent) * 100 702 | } 703 | 704 | s := Statistics{ 705 | PacketsSent: sent, 706 | PacketsRecv: p.PacketsRecv, 707 | PacketsRecvDuplicates: p.PacketsRecvDuplicates, 708 | PacketLoss: loss, 709 | Rtts: p.rtts, 710 | TTLs: p.ttls, 711 | Addr: p.addr, 712 | IPAddr: p.ipaddr, 713 | MaxRtt: p.maxRtt, 714 | MinRtt: p.minRtt, 715 | AvgRtt: p.avgRtt, 716 | StdDevRtt: p.stdDevRtt, 717 | } 718 | return &s 719 | } 720 | 721 | type expBackoff struct { 722 | baseDelay time.Duration 723 | maxExp int64 724 | c int64 725 | } 726 | 727 | func (b *expBackoff) Get() time.Duration { 728 | if b.c < b.maxExp { 729 | b.c++ 730 | } 731 | 732 | return b.baseDelay * time.Duration(rand.Int63n(1< 0 { 908 | t = append(t, bytes.Repeat([]byte{1}, remainSize)...) 909 | } 910 | 911 | body := &icmp.Echo{ 912 | ID: p.id, 913 | Seq: p.sequence, 914 | Data: t, 915 | } 916 | 917 | msg := &icmp.Message{ 918 | Type: conn.ICMPRequestType(), 919 | Code: 0, 920 | Body: body, 921 | } 922 | 923 | msgBytes, err := msg.Marshal(nil) 924 | if err != nil { 925 | return err 926 | } 927 | 928 | for { 929 | if _, err := conn.WriteTo(msgBytes, dst); err != nil { 930 | // Try to set broadcast flag 931 | if errors.Is(err, syscall.EACCES) && runtime.GOOS == "linux" { 932 | if e := conn.SetBroadcastFlag(); e != nil { 933 | p.logger.Warnf("had EACCES syscall error, check your local firewall") 934 | } 935 | p.logger.Infof("Pinging a broadcast address") 936 | continue 937 | } 938 | if p.OnSendError != nil { 939 | outPkt := &Packet{ 940 | Nbytes: len(msgBytes), 941 | IPAddr: p.ipaddr, 942 | Addr: p.addr, 943 | Seq: p.sequence, 944 | ID: p.id, 945 | } 946 | p.OnSendError(outPkt, err) 947 | } 948 | if neterr, ok := err.(*net.OpError); ok { 949 | if neterr.Err == syscall.ENOBUFS { 950 | continue 951 | } 952 | } 953 | return err 954 | } 955 | if p.OnSend != nil { 956 | outPkt := &Packet{ 957 | Nbytes: len(msgBytes), 958 | IPAddr: p.ipaddr, 959 | Addr: p.addr, 960 | Seq: p.sequence, 961 | ID: p.id, 962 | } 963 | p.OnSend(outPkt) 964 | } 965 | // mark this sequence as in-flight 966 | p.awaitingSequences[currentUUID][p.sequence] = struct{}{} 967 | p.statsMu.Lock() 968 | p.PacketsSent++ 969 | p.statsMu.Unlock() 970 | p.sequence++ 971 | if p.sequence > 65535 { 972 | newUUID := uuid.New() 973 | p.trackerUUIDs = append(p.trackerUUIDs, newUUID) 974 | p.awaitingSequences[newUUID] = make(map[int]struct{}) 975 | p.sequence = 0 976 | } 977 | break 978 | } 979 | 980 | return nil 981 | } 982 | 983 | func (p *Pinger) listen() (packetConn, error) { 984 | var ( 985 | conn packetConn 986 | err error 987 | ) 988 | 989 | if p.ipv4 { 990 | var c icmpv4Conn 991 | c.c, err = icmp.ListenPacket(ipv4Proto[p.protocol], p.Source) 992 | conn = &c 993 | } else { 994 | var c icmpV6Conn 995 | c.c, err = icmp.ListenPacket(ipv6Proto[p.protocol], p.Source) 996 | conn = &c 997 | } 998 | 999 | if err != nil { 1000 | p.Stop() 1001 | return nil, err 1002 | } 1003 | 1004 | if p.Privileged() { 1005 | if err := conn.InstallICMPIDFilter(p.id); err != nil { 1006 | p.logger.Warnf("error installing icmp filter, %v", err) 1007 | } 1008 | } 1009 | 1010 | return conn, nil 1011 | } 1012 | 1013 | func bytesToTime(b []byte) time.Time { 1014 | var nsec int64 1015 | for i := uint8(0); i < 8; i++ { 1016 | nsec += int64(b[i]) << ((7 - i) * 8) 1017 | } 1018 | return time.Unix(nsec/1000000000, nsec%1000000000) 1019 | } 1020 | 1021 | func isIPv4(ip net.IP) bool { 1022 | return len(ip.To4()) == net.IPv4len 1023 | } 1024 | 1025 | func timeToBytes(t time.Time) []byte { 1026 | nsec := t.UnixNano() 1027 | b := make([]byte, 8) 1028 | for i := uint8(0); i < 8; i++ { 1029 | b[i] = byte((nsec >> ((7 - i) * 8)) & 0xff) 1030 | } 1031 | return b 1032 | } 1033 | 1034 | var seed = time.Now().UnixNano() 1035 | 1036 | // getSeed returns a goroutine-safe unique seed 1037 | func getSeed() int64 { 1038 | return atomic.AddInt64(&seed, 1) 1039 | } 1040 | 1041 | // stripIPv4Header strips IPv4 header bytes if present 1042 | // https://github.com/golang/go/commit/3b5be4522a21df8ce52a06a0c4ba005c89a8590f 1043 | func stripIPv4Header(n int, b []byte) int { 1044 | if len(b) < 20 { 1045 | return n 1046 | } 1047 | l := int(b[0]&0x0f) << 2 1048 | if 20 > l || l > len(b) { 1049 | return n 1050 | } 1051 | if b[0]>>4 != 4 { 1052 | return n 1053 | } 1054 | copy(b, b[l:]) 1055 | return n - l 1056 | } 1057 | --------------------------------------------------------------------------------