The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .circleci
    └── config.yml
├── .dockerignore
├── .github
    ├── ISSUE_TEMPLATE.md
    ├── dependabot.yml
    └── workflows
    │   ├── container_description.yml
    │   ├── golangci-lint.yml
    │   └── stale.yml
├── .gitignore
├── .golangci.yml
├── .promu.yml
├── .yamllint
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONFIGURATION.md
├── Dockerfile
├── LICENSE
├── MAINTAINERS.md
├── Makefile
├── Makefile.common
├── NOTICE
├── README.md
├── SECURITY.md
├── VERSION
├── blackbox.yml
├── config
    ├── config.go
    ├── config_test.go
    └── testdata
    │   ├── blackbox-bad.yml
    │   ├── blackbox-bad2.yml
    │   ├── blackbox-good.yml
    │   ├── invalid-dns-class.yml
    │   ├── invalid-dns-module.yml
    │   ├── invalid-dns-type.yml
    │   ├── invalid-http-body-config.yml
    │   ├── invalid-http-body-match-regexp.yml
    │   ├── invalid-http-body-not-match-regexp.yml
    │   ├── invalid-http-compression-mismatch-special-case.yml
    │   ├── invalid-http-compression-mismatch.yml
    │   ├── invalid-http-header-match-regexp.yml
    │   ├── invalid-http-header-match.yml
    │   ├── invalid-http-request-compression-reject-all-encodings.yml
    │   ├── invalid-icmp-ttl-overflow.yml
    │   ├── invalid-icmp-ttl.yml
    │   └── invalid-tcp-query-response-regexp.yml
├── example.yml
├── go.mod
├── go.sum
├── main.go
├── main_test.go
└── prober
    ├── dns.go
    ├── dns_test.go
    ├── grpc.go
    ├── grpc_test.go
    ├── handler.go
    ├── handler_test.go
    ├── history.go
    ├── history_test.go
    ├── http.go
    ├── http_test.go
    ├── icmp.go
    ├── prober.go
    ├── tcp.go
    ├── tcp_test.go
    ├── tls.go
    ├── utils.go
    └── utils_test.go


/.circleci/config.yml:
--------------------------------------------------------------------------------
 1 | version: 2.1
 2 | orbs:
 3 |   prometheus: prometheus/prometheus@0.17.1
 4 | executors:
 5 |   # Whenever the Go version is updated here, .promu.yml should also be updated.
 6 |   golang:
 7 |     docker:
 8 |       - image: cimg/go:1.24
 9 | jobs:
10 |   test:
11 |     executor: golang
12 |     steps:
13 |       - prometheus/setup_environment
14 |       - run: make
15 |       - prometheus/store_artifact:
16 |           file: blackbox_exporter
17 |       - run: git diff --exit-code
18 |   # IPv6 tests require the machine executor.
19 |   # See https://circleci.com/docs/2.0/faq/#can-i-use-ipv6-in-my-tests for details.
20 |   test-ipv6:
21 |     machine: true
22 |     working_directory: /home/circleci/.go_workspace/src/github.com/prometheus/blackbox_exporter
23 |     # Whenever the Go version is updated here, .promu.yml should also be updated.
24 |     environment:
25 |       DOCKER_TEST_IMAGE_NAME: quay.io/prometheus/golang-builder:1.24-base
26 |     steps:
27 |       - checkout
28 |       - run:
29 |           name: enable ipv6
30 |           command: |
31 |             cat \<<'EOF' | sudo tee /etc/docker/daemon.json
32 |             {
33 |               "ipv6": true,
34 |               "fixed-cidr-v6": "2001:db8:1::/64"
35 |             }
36 |             EOF
37 |             sudo service docker restart
38 |       - run: docker run --rm -t -v "$(pwd):/app" "${DOCKER_TEST_IMAGE_NAME}" -i github.com/prometheus/blackbox_exporter -T
39 | workflows:
40 |   version: 2
41 |   blackbox_exporter:
42 |     jobs:
43 |       - test:
44 |           filters:
45 |             tags:
46 |               only: /.*/
47 |       - test-ipv6:
48 |           filters:
49 |             tags:
50 |               only: /.*/
51 |       - prometheus/build:
52 |           name: build
53 |           filters:
54 |             tags:
55 |               only: /.*/
56 |       - prometheus/publish_master:
57 |           context: org-context
58 |           requires:
59 |             - test
60 |             - test-ipv6
61 |             - build
62 |           filters:
63 |             branches:
64 |               only: master
65 |       - prometheus/publish_release:
66 |           context: org-context
67 |           requires:
68 |             - test
69 |             - test-ipv6
70 |             - build
71 |           filters:
72 |             tags:
73 |               only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/
74 |             branches:
75 |               ignore: /.*/
76 | 


--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .build/
2 | .tarballs/
3 | 
4 | !.build/linux-amd64/
5 | !.build/linux-armv7
6 | !.build/linux-arm64
7 | !.build/linux-ppc64le
8 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
 1 | <!--
 2 | 	Please note: GitHub issues should only be used for feature requests and
 3 | 	bug reports. For general discussions, please refer to one of the community channels
 4 | 	described in https://prometheus.io/community/.
 5 | 
 6 | 	For bug reports, please fill out the below fields and provide as much detail
 7 | 	as possible about your issue.  For feature requests, you may omit the
 8 | 	following template.
 9 | -->
10 | ### Host operating system: output of `uname -a`
11 | 
12 | ### blackbox_exporter version: output of `blackbox_exporter --version`
13 | <!-- If building from source, run `make` first. -->
14 | 
15 | ### What is the blackbox.yml module config.
16 | 
17 | ### What is the prometheus.yml scrape config.
18 | 
19 | ### What logging output did you get from adding `&debug=true` to the probe URL?
20 | 
21 | ### What did you do that produced an error?
22 | 
23 | ### What did you expect to see?
24 | 
25 | ### What did you see instead?
26 | 


--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 |   - package-ecosystem: "gomod"
4 |     directory: "/"
5 |     schedule:
6 |       interval: "monthly"
7 | 


--------------------------------------------------------------------------------
/.github/workflows/container_description.yml:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: Push README to Docker Hub
 3 | on:
 4 |   push:
 5 |     paths:
 6 |       - "README.md"
 7 |       - "README-containers.md"
 8 |       - ".github/workflows/container_description.yml"
 9 |     branches: [ main, master ]
10 | 
11 | permissions:
12 |   contents: read
13 | 
14 | jobs:
15 |   PushDockerHubReadme:
16 |     runs-on: ubuntu-latest
17 |     name: Push README to Docker Hub
18 |     if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks.
19 |     steps:
20 |       - name: git checkout
21 |         uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
22 |         with:
23 |           persist-credentials: false
24 |       - name: Set docker hub repo name
25 |         run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV
26 |       - name: Push README to Dockerhub
27 |         uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1
28 |         env:
29 |           DOCKER_USER: ${{ secrets.DOCKER_HUB_LOGIN }}
30 |           DOCKER_PASS: ${{ secrets.DOCKER_HUB_PASSWORD }}
31 |         with:
32 |           destination_container_repo: ${{ env.DOCKER_REPO_NAME }}
33 |           provider: dockerhub
34 |           short_description: ${{ env.DOCKER_REPO_NAME }}
35 |           # Empty string results in README-containers.md being pushed if it
36 |           # exists. Otherwise, README.md is pushed.
37 |           readme_file: ''
38 | 
39 |   PushQuayIoReadme:
40 |     runs-on: ubuntu-latest
41 |     name: Push README to quay.io
42 |     if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks.
43 |     steps:
44 |       - name: git checkout
45 |         uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
46 |         with:
47 |           persist-credentials: false
48 |       - name: Set quay.io org name
49 |         run: echo "DOCKER_REPO=$(echo quay.io/${GITHUB_REPOSITORY_OWNER} | tr -d '-')" >> $GITHUB_ENV
50 |       - name: Set quay.io repo name
51 |         run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV
52 |       - name: Push README to quay.io
53 |         uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1
54 |         env:
55 |           DOCKER_APIKEY: ${{ secrets.QUAY_IO_API_TOKEN }}
56 |         with:
57 |           destination_container_repo: ${{ env.DOCKER_REPO_NAME }}
58 |           provider: quay
59 |           # Empty string results in README-containers.md being pushed if it
60 |           # exists. Otherwise, README.md is pushed.
61 |           readme_file: ''
62 | 


--------------------------------------------------------------------------------
/.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@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
32 |         with:
33 |           go-version: 1.24.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: Lint
38 |         uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
39 |         with:
40 |           args: --verbose
41 |           version: v2.2.1
42 | 


--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
 1 | name: Stale Check
 2 | on:
 3 |   workflow_dispatch: {}
 4 |   schedule:
 5 |     - cron: '16 22 * * *'
 6 | permissions:
 7 |   issues: write
 8 |   pull-requests: write
 9 | jobs:
10 |   stale:
11 |     if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks.
12 |     runs-on: ubuntu-latest
13 |     steps:
14 |       - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
15 |         with:
16 |           repo-token: ${{ secrets.GITHUB_TOKEN }}
17 |           # opt out of defaults to avoid marking issues as stale and closing them
18 |           # https://github.com/actions/stale#days-before-close
19 |           # https://github.com/actions/stale#days-before-stale
20 |           days-before-stale: -1
21 |           days-before-close: -1
22 |           # Setting it to empty string to skip comments.
23 |           # https://github.com/actions/stale#stale-pr-message
24 |           # https://github.com/actions/stale#stale-issue-message
25 |           stale-pr-message: ''
26 |           stale-issue-message: ''
27 |           operations-per-run: 30
28 |           # override days-before-stale, for only marking the pull requests as stale
29 |           days-before-pr-stale: 60
30 |           stale-pr-label: stale
31 |           exempt-pr-labels: keepalive
32 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
 2 | *.o
 3 | *.a
 4 | *.so
 5 | 
 6 | # Folders
 7 | _obj
 8 | _test
 9 | 
10 | # Architecture specific extensions/prefixes
11 | *.[568vq]
12 | [568vq].out
13 | 
14 | *.cgo1.go
15 | *.cgo2.c
16 | _cgo_defun.c
17 | _cgo_gotypes.go
18 | _cgo_export.*
19 | 
20 | _testmain.go
21 | 
22 | *.exe
23 | dependencies-stamp
24 | /blackbox_exporter
25 | /.build
26 | /.release
27 | /.tarballs
28 | .deps
29 | *.tar.gz
30 | /vendor
31 | 


--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
 1 | version: "2"
 2 | linters:
 3 |   default: none
 4 |   enable:
 5 |     - misspell
 6 |     - sloglint
 7 |     - staticcheck
 8 |   exclusions:
 9 |     generated: lax
10 |     presets:
11 |       - comments
12 |       - common-false-positives
13 |       - legacy
14 |       - std-error-handling
15 |     paths:
16 |       - third_party$
17 |       - builtin$
18 |       - examples$
19 | formatters:
20 |   exclusions:
21 |     generated: lax
22 |     paths:
23 |       - third_party$
24 |       - builtin$
25 |       - examples$
26 | 


--------------------------------------------------------------------------------
/.promu.yml:
--------------------------------------------------------------------------------
 1 | go:
 2 |     # Whenever the Go version is updated here, .circle/config.yml should also be updated.
 3 |     version: 1.24
 4 | repository:
 5 |     path: github.com/prometheus/blackbox_exporter
 6 | build:
 7 |     ldflags: |
 8 |         -X github.com/prometheus/common/version.Version={{.Version}}
 9 |         -X github.com/prometheus/common/version.Revision={{.Revision}}
10 |         -X github.com/prometheus/common/version.Branch={{.Branch}}
11 |         -X github.com/prometheus/common/version.BuildUser={{user}}@{{host}}
12 |         -X github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}}
13 | tarball:
14 |     files:
15 |         - blackbox.yml
16 |         - LICENSE
17 |         - NOTICE
18 | 


--------------------------------------------------------------------------------
/.yamllint:
--------------------------------------------------------------------------------
 1 | ---
 2 | extends: default
 3 | ignore: |
 4 |   **/node_modules
 5 | 
 6 | rules:
 7 |   braces:
 8 |     max-spaces-inside: 1
 9 |     level: error
10 |   brackets:
11 |     max-spaces-inside: 1
12 |     level: error
13 |   commas: disable
14 |   comments: disable
15 |   comments-indentation: disable
16 |   document-start: disable
17 |   indentation:
18 |     spaces: consistent
19 |     indent-sequences: consistent
20 |   key-duplicates:
21 |     ignore: |
22 |       config/testdata/section_key_dup.bad.yml
23 |   line-length: disable
24 |   truthy:
25 |     check-keys: false
26 | 


--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
  1 | ## master / unreleased
  2 | 
  3 | BREAKING CHANGES:
  4 | 
  5 | Changes:
  6 | 
  7 | * [CHANGE]
  8 | * [FEATURE]
  9 | * [ENHANCEMENT]
 10 | * [BUGFIX]
 11 | 
 12 | ## 0.27.0 / 2025-06-26
 13 | 
 14 | * [FEATURE] Support matching JSON body with CEL expressions #1255
 15 | * [BUGFIX] Fix condition when local dns lookup should happen #1272
 16 | * [BUGFIX] Stop scrape logger spam #1381
 17 | 
 18 | ## 0.26.0 / 2025-02-26
 19 | 
 20 | * [CHANGE] adopt log/slog, drop go-kit/log #1311
 21 | * [FEATURE] Add metric to record tls ciphersuite negotiated during handshake #1203
 22 | * [FEATURE] Add a way to export labels with content matched by the probe #1284
 23 | * [FEATURE] Reports Certificate Serial number #1333
 24 | * [ENHANCEMENT] Enable misspell linter #1248
 25 | * [ENHANCEMENT] Fix incorrect parameters name in documentation #1126
 26 | * [ENHANCEMENT] Add stale workflow to start sync with stale.yaml in prometheus #1170
 27 | * [ENHANCEMENT] Update CONFIGURATION.md to clarify that valid_status_codes expects a list #1335
 28 | * [ENHANCEMENT] Skip failing IPv6 tests in test CI Pipeline #1342
 29 | * [ENHANCEMENT] Add RabbitMQ probe example #1349
 30 | * [BUGFIX] Only register grpc TLS metrics on successful handshake #1338
 31 | 
 32 | ## 0.25.0 / 2024-04-09
 33 | 
 34 | * [FEATURE] Allow to get Probe logs by target #1063
 35 | * [FEATURE] Log errors from probe #1091
 36 | * [BUGFIX] Prevent logging confusing error message #1059
 37 | * [BUGFIX] Explicit registration of internal exporter metrics 1060
 38 | 
 39 | ## 0.24.0 / 2023-05-16
 40 | 
 41 | * [CHANGE] Make Proxy Connect Headers consistent with Prometheus #1008
 42 | * [FEATURE] Add hostname parameter for TCP probe #981
 43 | * [FEATURE] Add support for HTTP request body as file #987
 44 | 
 45 | ## 0.23.0 / 2022-12-02
 46 | 
 47 | * [SECURITY] Update Exporter Toolkit (CVE-2022-46146) #979
 48 | * [FEATURE] Support multiple Listen Addresses and systemd socket activation #979
 49 | * [FEATURE] Add leaf certificate details in a new `probe_ssl_last_chain_info` metric. #943
 50 | * [FEATURE] DNS: Add `Add probe_dns_query_succeeded` metric. #990
 51 | 
 52 | ## 0.22.0 / 2022-08-02
 53 | 
 54 | * [FEATURE] HTTP: Add `skip_resolve_phase_with_proxy` option. #944
 55 | * [ENHANCEMENT] OAuth: Use Blackbox Exporter user agent when doing OAuth2
 56 |   authenticated requests. #948
 57 | * [ENHANCEMENT] Print usage and help to stdout instead of stderr. #928
 58 | 
 59 | 
 60 | ## 0.21.1 / 2022-06-17
 61 | 
 62 | * [BUGFIX] Fix a data race in HTTP probes. #929
 63 | 
 64 | ## 0.21.0 / 2022-05-30
 65 | 
 66 | This Prometheus release is built with go1.18, which contains two noticeable
 67 | changes related to TLS and HTTP:
 68 | 
 69 | 1. [TLS 1.0 and 1.1 disabled by default client-side](https://go.dev/doc/go1.18#tls10).
 70 |    Blackbox Exporter users can override this with the `min_version` parameter of
 71 |    [tls_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#tls_config).
 72 | 2. [Certificates signed with the SHA-1 hash function are rejected](https://go.dev/doc/go1.18#sha1).
 73 |    This doesn't apply to self-signed root certificates.
 74 | 
 75 | * [BUGFIX] Prevent setting negative timeouts when using a small scrape interval. #869
 76 | 
 77 | ## 0.20.0 / 2022-03-16
 78 | 
 79 | * [FEATURE] Add support for grpc health check. #835
 80 | * [FEATURE] Add hostname parameter. #823
 81 | * [ENHANCEMENT] Add body_size_limit option to http module. #836
 82 | * [ENHANCEMENT] Change default user agent. #557
 83 | * [ENHANCEMENT] Add control of recursion desired flag for DNS probes. #859
 84 | * [ENHANCEMENT] Delay init of http phase values. #865
 85 | * [BUGFIX] Fix IP hash. #863
 86 | 
 87 | ## 0.19.0 / 2021-05-10
 88 | 
 89 | In the HTTP probe, `no_follow_redirects` has been changed to `follow_redirects`.
 90 | This release accepts both, with a precedence to the `no_follow_redirects` parameter.
 91 | In the next release, `no_follow_redirects` will be removed.
 92 | 
 93 | * [CHANGE] HTTP probe: `no_follow_redirects` has been renamed to `follow_redirects`. #784
 94 | * [FEATURE] Add support for decompression of HTTP responses. #764
 95 | * [FEATURE] Enable TLS and basic authentication. #730
 96 | * [FEATURE] HTTP probe: *experimental* OAuth2 support. #784
 97 | * [ENHANCEMENT] Add a health endpoint. #752
 98 | * [ENHANCEMENT] Add a metric for unknown probes. #716
 99 | * [ENHANCEMENT] Use preferred protocol first when resolving hostname. #728
100 | * [ENHANCEMENT] Validating the configuration tries to compile regexes. #729
101 | * [BUGFIX] HTTP probe: Fix error checking. #723
102 | * [BUGFIX] HTTP probe: Fix how the TLS phase is calculated. #758
103 | 


--------------------------------------------------------------------------------
/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 | 


--------------------------------------------------------------------------------
/CONFIGURATION.md:
--------------------------------------------------------------------------------
  1 | # Blackbox exporter configuration
  2 | 
  3 | The file is written in [YAML format](http://en.wikipedia.org/wiki/YAML), defined by the scheme described below.
  4 | Brackets indicate that a parameter is optional.
  5 | For non-list parameters the value is set to the specified default.
  6 | 
  7 | Generic placeholders are defined as follows:
  8 | 
  9 | * `<boolean>`: a boolean that can take the values `true` or `false`
 10 | * `<int>`: a regular integer
 11 | * `<duration>`: a duration matching the regular expression `[0-9]+(ms|[smhdwy])`
 12 | * `<filename>`: a valid path in the current working directory
 13 | * `<string>`: a regular string
 14 | * `<secret>`: a regular string that is a secret, such as a password
 15 | * `<regex>`: a regular expression
 16 | 
 17 | The other placeholders are specified separately.
 18 | 
 19 | See [example.yml](example.yml) for configuration examples.
 20 | 
 21 | ```yml
 22 | 
 23 | modules:
 24 |      [ <string>: <module> ... ]
 25 | 
 26 | ```
 27 | 
 28 | 
 29 | ### `<module>`
 30 | ```yml
 31 | 
 32 |   # The protocol over which the probe will take place (http, tcp, dns, icmp, grpc).
 33 |   prober: <prober_string>
 34 | 
 35 |   # How long the probe will wait before giving up.
 36 |   [ timeout: <duration> ]
 37 | 
 38 |   # The specific probe configuration - at most one of these should be specified.
 39 |   [ http: <http_probe> ]
 40 |   [ tcp: <tcp_probe> ]
 41 |   [ dns: <dns_probe> ]
 42 |   [ icmp: <icmp_probe> ]
 43 |   [ grpc: <grpc_probe> ]
 44 | 
 45 | ```
 46 | 
 47 | ### `<http_probe>`
 48 | ```yml
 49 | 
 50 |   # Accepted status codes for this probe. List between square brackets. Defaults to 2xx.
 51 |   [ valid_status_codes: [<int>, ...] | default = 2xx ]
 52 | 
 53 |   # Accepted HTTP versions for this probe.
 54 |   [ valid_http_versions: <string>, ... ]
 55 | 
 56 |   # The HTTP method the probe will use.
 57 |   [ method: <string> | default = "GET" ]
 58 | 
 59 |   # The HTTP headers set for the probe.
 60 |   headers:
 61 |     [ <string>: <string> ... ]
 62 | 
 63 |   # The maximum uncompressed body length in bytes that will be processed. A value of 0 means no limit.
 64 |   #
 65 |   # If the response includes a Content-Length header, it is NOT validated against this value. This
 66 |   # setting is only meant to limit the amount of data that you are willing to read from the server.
 67 |   #
 68 |   # Example: 10MB
 69 |   [ body_size_limit: <size> | default = 0 ]
 70 | 
 71 |   # The compression algorithm to use to decompress the response (gzip, br, deflate, identity).
 72 |   #
 73 |   # If an "Accept-Encoding" header is specified, it MUST be such that the compression algorithm
 74 |   # indicated using this option is acceptable. For example, you can use `compression: gzip` and
 75 |   # `Accept-Encoding: br, gzip` or `Accept-Encoding: br;q=1.0, gzip;q=0.9`. The fact that gzip is
 76 |   # acceptable with a lower quality than br does not invalidate the configuration, as you might
 77 |   # be testing that the server does not return br-encoded content even if it's requested. On the
 78 |   # other hand, `compression: gzip` and `Accept-Encoding: br, identity` is NOT a valid
 79 |   # configuration, because you are asking for gzip to NOT be returned, and trying to decompress
 80 |   # whatever the server returns is likely going to fail.
 81 |   [ compression: <string> | default = "" ]
 82 | 
 83 |   # Whether or not the probe will follow any redirects.
 84 |   [ follow_redirects: <boolean> | default = true ]
 85 | 
 86 |   # Probe fails if SSL is present.
 87 |   [ fail_if_ssl: <boolean> | default = false ]
 88 | 
 89 |   # Probe fails if SSL is not present.
 90 |   [ fail_if_not_ssl: <boolean> | default = false ]
 91 | 
 92 |   # Probe fails if response body JSON matches the CEL expression or if response is not JSON. See: https://github.com/google/cel-spec:
 93 |   fail_if_body_json_matches_cel: <string>
 94 | 
 95 |   # Probe fails if response body JSON does not match CEL expression or if response is not JSON. See: https://github.com/google/cel-spec:
 96 |   fail_if_body_json_not_matches_cel: <string>
 97 | 
 98 |   # Probe fails if response body matches regex.
 99 |   fail_if_body_matches_regexp:
100 |     [ - <regex>, ... ]
101 | 
102 |   # Probe fails if response body does not match regex.
103 |   fail_if_body_not_matches_regexp:
104 |     [ - <regex>, ... ]
105 | 
106 |   # Probe fails if response header matches regex. For headers with multiple values, fails if *at least one* matches.
107 |   fail_if_header_matches:
108 |     [ - <http_header_match_spec>, ... ]
109 | 
110 |   # Probe fails if response header does not match regex. For headers with multiple values, fails if *none* match.
111 |   fail_if_header_not_matches:
112 |     [ - <http_header_match_spec>, ... ]
113 | 
114 |   # Configuration for TLS protocol of HTTP probe.
115 |   tls_config:
116 |     [ <tls_config> ]
117 | 
118 |   # The HTTP basic authentication credentials.
119 |   basic_auth:
120 |     [ username: <string> ]
121 |     [ password: <secret> ]
122 |     [ password_file: <filename> ]
123 | 
124 |   # Sets the `Authorization` header on every request with
125 |   # the configured credentials.
126 |   authorization:
127 |     # Sets the authentication type of the request.
128 |     [ type: <string> | default: Bearer ]
129 |     # Sets the credentials of the request. It is mutually exclusive with
130 |     # `credentials_file`.
131 |     [ credentials: <secret> ]
132 |     # Sets the credentials of the request with the credentials read from the
133 |     # configured file. It is mutually exclusive with `credentials`.
134 |     [ credentials_file: <filename> ]
135 | 
136 |   # HTTP proxy server to use to connect to the targets.
137 |   [ proxy_url: <string> ]
138 |   # Comma-separated string that can contain IPs, CIDR notation, domain names
139 |   # that should be excluded from proxying. IP and domain names can
140 |   # contain port numbers.
141 |   [ no_proxy: <string> ]
142 |   # Use proxy URL indicated by environment variables (HTTP_PROXY, https_proxy, HTTPs_PROXY, https_proxy, and no_proxy)
143 |   [ proxy_from_environment: <bool> | default: false ]
144 |   # Specifies headers to send to proxies during CONNECT requests.
145 |   [ proxy_connect_header:
146 |     [ <string>: [<secret>, ...] ] ]
147 | 
148 |   # Skip DNS resolution and URL change when an HTTP proxy (proxy_url or proxy_from_environment) is set.
149 |   [ skip_resolve_phase_with_proxy: <boolean> | default = false ]
150 | 
151 |   # OAuth 2.0 configuration to use to connect to the targets.
152 |   oauth2:
153 |       [ <oauth2> ]
154 | 
155 |   # Whether to enable HTTP2.
156 |   [ enable_http2: <bool> | default: true ]
157 | 
158 |   # The IP protocol of the HTTP probe (ip4, ip6).
159 |   [ preferred_ip_protocol: <string> | default = "ip6" ]
160 |   [ ip_protocol_fallback: <boolean> | default = true ]
161 | 
162 |   # The body of the HTTP request used in probe.
163 |   [ body: <string> ]
164 | 
165 |   # Read the HTTP request body from from a file.
166 |   # It is mutually exclusive with `body`.
167 |   [ body_file: <filename> ]
168 | 
169 | ```
170 | 
171 | #### `<http_header_match_spec>`
172 | 
173 | ```yml
174 | header: <string>,
175 | regexp: <regex>,
176 | [ allow_missing: <boolean> | default = false ]
177 | ```
178 | 
179 | ### `<tcp_probe>`
180 | 
181 | ```yml
182 | 
183 | # The IP protocol of the TCP probe (ip4, ip6).
184 | [ preferred_ip_protocol: <string> | default = "ip6" ]
185 | [ ip_protocol_fallback: <boolean | default = true> ]
186 | 
187 | # The source IP address.
188 | [ source_ip_address: <string> ]
189 | 
190 | # The query sent in the TCP probe and the expected associated response.
191 | # "expect" matches a regular expression;
192 | # "labels" can define labels which will be exported on metric "probe_expect_info";
193 | # "send" sends some content;
194 | # "send" and "labels.value" can contain values matched by "expect" (such as "${1}");
195 | # "starttls" upgrades TCP connection to TLS.
196 | query_response:
197 |   [ - [ [ expect: <string> ],
198 |         [ labels:
199 |           - [ name: <string>
200 |               value: <string>
201 |             ], ...
202 |         ],
203 |         [ send: <string> ],
204 |         [ starttls: <boolean | default = false> ]
205 |       ], ...
206 |   ]
207 | 
208 | # Whether or not TLS is used when the connection is initiated.
209 | [ tls: <boolean | default = false> ]
210 | 
211 | # Configuration for TLS protocol of TCP probe.
212 | tls_config:
213 |   [ <tls_config> ]
214 | 
215 | ```
216 | 
217 | ### `<dns_probe>`
218 | 
219 | ```yml
220 | 
221 | # The IP protocol of the DNS probe (ip4, ip6).
222 | [ preferred_ip_protocol: <string> | default = "ip6" ]
223 | [ ip_protocol_fallback: <boolean | default = true> ]
224 | 
225 | # The source IP address.
226 | [ source_ip_address: <string> ]
227 | 
228 | [ transport_protocol: <string> | default = "udp" ] # udp, tcp
229 | 
230 | # Whether to use DNS over TLS. This only works with TCP.
231 | [ dns_over_tls: <boolean | default = false> ]
232 | 
233 | # Configuration for TLS protocol of DNS over TLS probe.
234 | tls_config:
235 |   [ <tls_config> ]
236 | 
237 | query_name: <string>
238 | 
239 | [ query_type: <string> | default = "ANY" ]
240 | [ query_class: <string> | default = "IN" ]
241 | 
242 | # Set the recursion desired (RD) flag in the request.
243 | [ recursion_desired: <boolean> | default = true ]
244 | 
245 | # List of valid response codes.
246 | valid_rcodes:
247 |   [ - <string> ... | default = "NOERROR" ]
248 | 
249 | validate_answer_rrs:
250 | 
251 |   fail_if_matches_regexp:
252 |     [ - <regex>, ... ]
253 | 
254 |   fail_if_all_match_regexp:
255 |     [ - <regex>, ... ]
256 | 
257 |   fail_if_not_matches_regexp:
258 |     [ - <regex>, ... ]
259 | 
260 |   fail_if_none_matches_regexp:
261 |     [ - <regex>, ... ]
262 | 
263 | validate_authority_rrs:
264 | 
265 |   fail_if_matches_regexp:
266 |     [ - <regex>, ... ]
267 | 
268 |   fail_if_all_match_regexp:
269 |     [ - <regex>, ... ]
270 | 
271 |   fail_if_not_matches_regexp:
272 |     [ - <regex>, ... ]
273 | 
274 |   fail_if_none_matches_regexp:
275 |     [ - <regex>, ... ]
276 | 
277 | validate_additional_rrs:
278 | 
279 |   fail_if_matches_regexp:
280 |     [ - <regex>, ... ]
281 | 
282 |   fail_if_all_match_regexp:
283 |     [ - <regex>, ... ]
284 | 
285 |   fail_if_not_matches_regexp:
286 |     [ - <regex>, ... ]
287 | 
288 |   fail_if_none_matches_regexp:
289 |     [ - <regex>, ... ]
290 | 
291 | ```
292 | 
293 | ### `<icmp_probe>`
294 | 
295 | ```yml
296 | 
297 | # The IP protocol of the ICMP probe (ip4, ip6).
298 | [ preferred_ip_protocol: <string> | default = "ip6" ]
299 | [ ip_protocol_fallback: <boolean | default = true> ]
300 | 
301 | # The source IP address.
302 | [ source_ip_address: <string> ]
303 | 
304 | # Set the DF-bit in the IP-header. Only works with ip4, on *nix systems and
305 | # requires raw sockets (i.e. root or CAP_NET_RAW on Linux).
306 | [ dont_fragment: <boolean> | default = false ]
307 | 
308 | # The size of the payload.
309 | [ payload_size: <int> ]
310 | 
311 | # TTL of outbound packets. Value must be in the range [0, 255]. Can be used
312 | # to test reachability of a target within a given number of hops, for example,
313 | # to determine when network routing has changed.
314 | [ ttl: <int> ]
315 | 
316 | ```
317 | 
318 | ### `<grpc_probe>`
319 | 
320 | ```yml
321 | # The service name to query for health status.
322 | [ service: <string> ]
323 | 
324 | # The IP protocol of the gRPC probe (ip4, ip6).
325 | [ preferred_ip_protocol: <string> ]
326 | [ ip_protocol_fallback: <boolean> | default = true ]
327 | 
328 | # Whether to connect to the endpoint with TLS.
329 | [ tls: <boolean | default = false> ]
330 | 
331 | # Configuration for TLS protocol of gRPC probe.
332 | tls_config:
333 |   [ <tls_config> ]
334 | ```
335 | 
336 | ### `<tls_config>`
337 | 
338 | ```yml
339 | 
340 | # Disable target certificate validation.
341 | [ insecure_skip_verify: <boolean> | default = false ]
342 | 
343 | # The CA cert to use for the targets.
344 | [ ca_file: <filename> ]
345 | 
346 | # The client cert file for the targets.
347 | [ cert_file: <filename> ]
348 | 
349 | # The client key file for the targets.
350 | [ key_file: <filename> ]
351 | 
352 | # Used to verify the hostname for the targets.
353 | [ server_name: <string> ]
354 | 
355 | # Minimum acceptable TLS version. Accepted values: TLS10 (TLS 1.0), TLS11 (TLS
356 | # 1.1), TLS12 (TLS 1.2), TLS13 (TLS 1.3).
357 | # If unset, Prometheus will use Go default minimum version, which is TLS 1.2.
358 | # See MinVersion in https://pkg.go.dev/crypto/tls#Config.
359 | [ min_version: <string> ]
360 | 
361 | # Maximum acceptable TLS version. Accepted values: TLS10 (TLS 1.0), TLS11 (TLS
362 | # 1.1), TLS12 (TLS 1.2), TLS13 (TLS 1.3).
363 | # Can be used to test for the presence of insecure TLS versions.
364 | # If unset, Prometheus will use Go default maximum version, which is TLS 1.3.
365 | # See MaxVersion in https://pkg.go.dev/crypto/tls#Config.
366 | [ max_version: <string> ]
367 | ```
368 | 
369 | #### `<oauth2>`
370 | 
371 | OAuth 2.0 authentication using the client credentials grant type. Blackbox
372 | exporter fetches an access token from the specified endpoint with the given
373 | client access and secret keys.
374 | 
375 | NOTE: This is *experimental* in the blackbox exporter and might not be
376 | reflected properly in the probe metrics at the moment.
377 | 
378 | ```yml
379 | client_id: <string>
380 | [ client_secret: <secret> ]
381 | 
382 | # Read the client secret from a file.
383 | # It is mutually exclusive with `client_secret`.
384 | [ client_secret_file: <filename> ]
385 | 
386 | # Scopes for the token request.
387 | scopes:
388 |   [ - <string> ... ]
389 | 
390 | # The URL to fetch the token from.
391 | token_url: <string>
392 | 
393 | # Optional parameters to append to the token URL.
394 | endpoint_params:
395 |   [ <string>: <string> ... ]
396 | ```
397 | 


--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
 1 | ARG ARCH="amd64"
 2 | ARG OS="linux"
 3 | FROM quay.io/prometheus/busybox-${OS}-${ARCH}:latest
 4 | LABEL maintainer="The Prometheus Authors <prometheus-developers@googlegroups.com>"
 5 | 
 6 | ARG ARCH="amd64"
 7 | ARG OS="linux"
 8 | COPY .build/${OS}-${ARCH}/blackbox_exporter  /bin/blackbox_exporter
 9 | COPY blackbox.yml       /etc/blackbox_exporter/config.yml
10 | 
11 | EXPOSE      9115
12 | ENTRYPOINT  [ "/bin/blackbox_exporter" ]
13 | CMD         [ "--config.file=/etc/blackbox_exporter/config.yml" ]
14 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
  1 |                                  Apache License
  2 |                            Version 2.0, January 2004
  3 |                         http://www.apache.org/licenses/
  4 | 
  5 |    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
  6 | 
  7 |    1. Definitions.
  8 | 
  9 |       "License" shall mean the terms and conditions for use, reproduction,
 10 |       and distribution as defined by Sections 1 through 9 of this document.
 11 | 
 12 |       "Licensor" shall mean the copyright owner or entity authorized by
 13 |       the copyright owner that is granting the License.
 14 | 
 15 |       "Legal Entity" shall mean the union of the acting entity and all
 16 |       other entities that control, are controlled by, or are under common
 17 |       control with that entity. For the purposes of this definition,
 18 |       "control" means (i) the power, direct or indirect, to cause the
 19 |       direction or management of such entity, whether by contract or
 20 |       otherwise, or (ii) ownership of fifty percent (50%) or more of the
 21 |       outstanding shares, or (iii) beneficial ownership of such entity.
 22 | 
 23 |       "You" (or "Your") shall mean an individual or Legal Entity
 24 |       exercising permissions granted by this License.
 25 | 
 26 |       "Source" form shall mean the preferred form for making modifications,
 27 |       including but not limited to software source code, documentation
 28 |       source, and configuration files.
 29 | 
 30 |       "Object" form shall mean any form resulting from mechanical
 31 |       transformation or translation of a Source form, including but
 32 |       not limited to compiled object code, generated documentation,
 33 |       and conversions to other media types.
 34 | 
 35 |       "Work" shall mean the work of authorship, whether in Source or
 36 |       Object form, made available under the License, as indicated by a
 37 |       copyright notice that is included in or attached to the work
 38 |       (an example is provided in the Appendix below).
 39 | 
 40 |       "Derivative Works" shall mean any work, whether in Source or Object
 41 |       form, that is based on (or derived from) the Work and for which the
 42 |       editorial revisions, annotations, elaborations, or other modifications
 43 |       represent, as a whole, an original work of authorship. For the purposes
 44 |       of this License, Derivative Works shall not include works that remain
 45 |       separable from, or merely link (or bind by name) to the interfaces of,
 46 |       the Work and Derivative Works thereof.
 47 | 
 48 |       "Contribution" shall mean any work of authorship, including
 49 |       the original version of the Work and any modifications or additions
 50 |       to that Work or Derivative Works thereof, that is intentionally
 51 |       submitted to Licensor for inclusion in the Work by the copyright owner
 52 |       or by an individual or Legal Entity authorized to submit on behalf of
 53 |       the copyright owner. For the purposes of this definition, "submitted"
 54 |       means any form of electronic, verbal, or written communication sent
 55 |       to the Licensor or its representatives, including but not limited to
 56 |       communication on electronic mailing lists, source code control systems,
 57 |       and issue tracking systems that are managed by, or on behalf of, the
 58 |       Licensor for the purpose of discussing and improving the Work, but
 59 |       excluding communication that is conspicuously marked or otherwise
 60 |       designated in writing by the copyright owner as "Not a Contribution."
 61 | 
 62 |       "Contributor" shall mean Licensor and any individual or Legal Entity
 63 |       on behalf of whom a Contribution has been received by Licensor and
 64 |       subsequently incorporated within the Work.
 65 | 
 66 |    2. Grant of Copyright License. Subject to the terms and conditions of
 67 |       this License, each Contributor hereby grants to You a perpetual,
 68 |       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 69 |       copyright license to reproduce, prepare Derivative Works of,
 70 |       publicly display, publicly perform, sublicense, and distribute the
 71 |       Work and such Derivative Works in Source or Object form.
 72 | 
 73 |    3. Grant of Patent License. Subject to the terms and conditions of
 74 |       this License, each Contributor hereby grants to You a perpetual,
 75 |       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 76 |       (except as stated in this section) patent license to make, have made,
 77 |       use, offer to sell, sell, import, and otherwise transfer the Work,
 78 |       where such license applies only to those patent claims licensable
 79 |       by such Contributor that are necessarily infringed by their
 80 |       Contribution(s) alone or by combination of their Contribution(s)
 81 |       with the Work to which such Contribution(s) was submitted. If You
 82 |       institute patent litigation against any entity (including a
 83 |       cross-claim or counterclaim in a lawsuit) alleging that the Work
 84 |       or a Contribution incorporated within the Work constitutes direct
 85 |       or contributory patent infringement, then any patent licenses
 86 |       granted to You under this License for that Work shall terminate
 87 |       as of the date such litigation is filed.
 88 | 
 89 |    4. Redistribution. You may reproduce and distribute copies of the
 90 |       Work or Derivative Works thereof in any medium, with or without
 91 |       modifications, and in Source or Object form, provided that You
 92 |       meet the following conditions:
 93 | 
 94 |       (a) You must give any other recipients of the Work or
 95 |           Derivative Works a copy of this License; and
 96 | 
 97 |       (b) You must cause any modified files to carry prominent notices
 98 |           stating that You changed the files; and
 99 | 
100 |       (c) You must retain, in the Source form of any Derivative Works
101 |           that You distribute, all copyright, patent, trademark, and
102 |           attribution notices from the Source form of the Work,
103 |           excluding those notices that do not pertain to any part of
104 |           the Derivative Works; and
105 | 
106 |       (d) If the Work includes a "NOTICE" text file as part of its
107 |           distribution, then any Derivative Works that You distribute must
108 |           include a readable copy of the attribution notices contained
109 |           within such NOTICE file, excluding those notices that do not
110 |           pertain to any part of the Derivative Works, in at least one
111 |           of the following places: within a NOTICE text file distributed
112 |           as part of the Derivative Works; within the Source form or
113 |           documentation, if provided along with the Derivative Works; or,
114 |           within a display generated by the Derivative Works, if and
115 |           wherever such third-party notices normally appear. The contents
116 |           of the NOTICE file are for informational purposes only and
117 |           do not modify the License. You may add Your own attribution
118 |           notices within Derivative Works that You distribute, alongside
119 |           or as an addendum to the NOTICE text from the Work, provided
120 |           that such additional attribution notices cannot be construed
121 |           as modifying the License.
122 | 
123 |       You may add Your own copyright statement to Your modifications and
124 |       may provide additional or different license terms and conditions
125 |       for use, reproduction, or distribution of Your modifications, or
126 |       for any such Derivative Works as a whole, provided Your use,
127 |       reproduction, and distribution of the Work otherwise complies with
128 |       the conditions stated in this License.
129 | 
130 |    5. Submission of Contributions. Unless You explicitly state otherwise,
131 |       any Contribution intentionally submitted for inclusion in the Work
132 |       by You to the Licensor shall be under the terms and conditions of
133 |       this License, without any additional terms or conditions.
134 |       Notwithstanding the above, nothing herein shall supersede or modify
135 |       the terms of any separate license agreement you may have executed
136 |       with Licensor regarding such Contributions.
137 | 
138 |    6. Trademarks. This License does not grant permission to use the trade
139 |       names, trademarks, service marks, or product names of the Licensor,
140 |       except as required for reasonable and customary use in describing the
141 |       origin of the Work and reproducing the content of the NOTICE file.
142 | 
143 |    7. Disclaimer of Warranty. Unless required by applicable law or
144 |       agreed to in writing, Licensor provides the Work (and each
145 |       Contributor provides its Contributions) on an "AS IS" BASIS,
146 |       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 |       implied, including, without limitation, any warranties or conditions
148 |       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 |       PARTICULAR PURPOSE. You are solely responsible for determining the
150 |       appropriateness of using or redistributing the Work and assume any
151 |       risks associated with Your exercise of permissions under this License.
152 | 
153 |    8. Limitation of Liability. In no event and under no legal theory,
154 |       whether in tort (including negligence), contract, or otherwise,
155 |       unless required by applicable law (such as deliberate and grossly
156 |       negligent acts) or agreed to in writing, shall any Contributor be
157 |       liable to You for damages, including any direct, indirect, special,
158 |       incidental, or consequential damages of any character arising as a
159 |       result of this License or out of the use or inability to use the
160 |       Work (including but not limited to damages for loss of goodwill,
161 |       work stoppage, computer failure or malfunction, or any and all
162 |       other commercial damages or losses), even if such Contributor
163 |       has been advised of the possibility of such damages.
164 | 
165 |    9. Accepting Warranty or Additional Liability. While redistributing
166 |       the Work or Derivative Works thereof, You may choose to offer,
167 |       and charge a fee for, acceptance of support, warranty, indemnity,
168 |       or other liability obligations and/or rights consistent with this
169 |       License. However, in accepting such obligations, You may act only
170 |       on Your own behalf and on Your sole responsibility, not on behalf
171 |       of any other Contributor, and only if You agree to indemnify,
172 |       defend, and hold each Contributor harmless for any liability
173 |       incurred by, or claims asserted against, such Contributor by reason
174 |       of your accepting any such warranty or additional liability.
175 | 
176 |    END OF TERMS AND CONDITIONS
177 | 
178 |    APPENDIX: How to apply the Apache License to your work.
179 | 
180 |       To apply the Apache License to your work, attach the following
181 |       boilerplate notice, with the fields enclosed by brackets "[]"
182 |       replaced with your own identifying information. (Don't include
183 |       the brackets!)  The text should be enclosed in the appropriate
184 |       comment syntax for the file format. We also recommend that a
185 |       file or class name and description of purpose be included on the
186 |       same "printed page" as the copyright notice for easier
187 |       identification within third-party archives.
188 | 
189 |    Copyright [yyyy] [name of copyright owner]
190 | 
191 |    Licensed under the Apache License, Version 2.0 (the "License");
192 |    you may not use this file except in compliance with the License.
193 |    You may obtain a copy of the License at
194 | 
195 |        http://www.apache.org/licenses/LICENSE-2.0
196 | 
197 |    Unless required by applicable law or agreed to in writing, software
198 |    distributed under the License is distributed on an "AS IS" BASIS,
199 |    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 |    See the License for the specific language governing permissions and
201 |    limitations under the License.
202 | 


--------------------------------------------------------------------------------
/MAINTAINERS.md:
--------------------------------------------------------------------------------
1 | * Julien Pivotto <roidelapluie@prometheus.io> @roidelapluie
2 | * Marcelo Magallon <marcelo.magallon@grafana.com> @mem
3 | * Suraj Nath <suraj.sidh@grafana.com> @electron0zero
4 | 


--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
 1 | # Copyright 2016 The Prometheus Authors
 2 | # Licensed under the Apache License, Version 2.0 (the "License");
 3 | # you may not use this file except in compliance with the License.
 4 | # You may obtain a copy of the License at
 5 | #
 6 | # http://www.apache.org/licenses/LICENSE-2.0
 7 | #
 8 | # Unless required by applicable law or agreed to in writing, software
 9 | # distributed under the License is distributed on an "AS IS" BASIS,
10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | # See the License for the specific language governing permissions and
12 | # limitations under the License.
13 | 
14 | # Needs to be defined before including Makefile.common to auto-generate targets
15 | DOCKER_ARCHS ?= amd64 armv7 arm64 ppc64le
16 | 
17 | include Makefile.common
18 | 
19 | DOCKER_IMAGE_NAME       ?= blackbox-exporter
20 | 


--------------------------------------------------------------------------------
/Makefile.common:
--------------------------------------------------------------------------------
  1 | # Copyright 2018 The Prometheus Authors
  2 | # Licensed under the Apache License, Version 2.0 (the "License");
  3 | # you may not use this file except in compliance with the License.
  4 | # You may obtain a copy of the License at
  5 | #
  6 | # http://www.apache.org/licenses/LICENSE-2.0
  7 | #
  8 | # Unless required by applicable law or agreed to in writing, software
  9 | # distributed under the License is distributed on an "AS IS" BASIS,
 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 11 | # See the License for the specific language governing permissions and
 12 | # limitations under the License.
 13 | 
 14 | 
 15 | # A common Makefile that includes rules to be reused in different prometheus projects.
 16 | # !!! Open PRs only against the prometheus/prometheus/Makefile.common repository!
 17 | 
 18 | # Example usage :
 19 | # Create the main Makefile in the root project directory.
 20 | # include Makefile.common
 21 | # customTarget:
 22 | # 	@echo ">> Running customTarget"
 23 | #
 24 | 
 25 | # Ensure GOBIN is not set during build so that promu is installed to the correct path
 26 | unexport GOBIN
 27 | 
 28 | GO           ?= go
 29 | GOFMT        ?= $(GO)fmt
 30 | FIRST_GOPATH := $(firstword $(subst :, ,$(shell $(GO) env GOPATH)))
 31 | GOOPTS       ?=
 32 | GOHOSTOS     ?= $(shell $(GO) env GOHOSTOS)
 33 | GOHOSTARCH   ?= $(shell $(GO) env GOHOSTARCH)
 34 | 
 35 | GO_VERSION        ?= $(shell $(GO) version)
 36 | GO_VERSION_NUMBER ?= $(word 3, $(GO_VERSION))
 37 | PRE_GO_111        ?= $(shell echo $(GO_VERSION_NUMBER) | grep -E 'go1\.(10|[0-9])\.')
 38 | 
 39 | PROMU        := $(FIRST_GOPATH)/bin/promu
 40 | pkgs          = ./...
 41 | 
 42 | ifeq (arm, $(GOHOSTARCH))
 43 | 	GOHOSTARM ?= $(shell GOARM= $(GO) env GOARM)
 44 | 	GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH)v$(GOHOSTARM)
 45 | else
 46 | 	GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH)
 47 | endif
 48 | 
 49 | GOTEST := $(GO) test
 50 | GOTEST_DIR :=
 51 | ifneq ($(CIRCLE_JOB),)
 52 | ifneq ($(shell command -v gotestsum 2> /dev/null),)
 53 | 	GOTEST_DIR := test-results
 54 | 	GOTEST := gotestsum --junitfile $(GOTEST_DIR)/unit-tests.xml --
 55 | endif
 56 | endif
 57 | 
 58 | PROMU_VERSION ?= 0.17.0
 59 | PROMU_URL     := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz
 60 | 
 61 | SKIP_GOLANGCI_LINT :=
 62 | GOLANGCI_LINT :=
 63 | GOLANGCI_LINT_OPTS ?=
 64 | GOLANGCI_LINT_VERSION ?= v2.2.1
 65 | GOLANGCI_FMT_OPTS ?=
 66 | # golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64.
 67 | # windows isn't included here because of the path separator being different.
 68 | ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin))
 69 | 	ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386 arm64))
 70 | 		# If we're in CI and there is an Actions file, that means the linter
 71 | 		# is being run in Actions, so we don't need to run it here.
 72 | 		ifneq (,$(SKIP_GOLANGCI_LINT))
 73 | 			GOLANGCI_LINT :=
 74 | 		else ifeq (,$(CIRCLE_JOB))
 75 | 			GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint
 76 | 		else ifeq (,$(wildcard .github/workflows/golangci-lint.yml))
 77 | 			GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint
 78 | 		endif
 79 | 	endif
 80 | endif
 81 | 
 82 | PREFIX                  ?= $(shell pwd)
 83 | BIN_DIR                 ?= $(shell pwd)
 84 | DOCKER_IMAGE_TAG        ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD))
 85 | DOCKERFILE_PATH         ?= ./Dockerfile
 86 | DOCKERBUILD_CONTEXT     ?= ./
 87 | DOCKER_REPO             ?= prom
 88 | 
 89 | DOCKER_ARCHS            ?= amd64
 90 | 
 91 | BUILD_DOCKER_ARCHS = $(addprefix common-docker-,$(DOCKER_ARCHS))
 92 | PUBLISH_DOCKER_ARCHS = $(addprefix common-docker-publish-,$(DOCKER_ARCHS))
 93 | TAG_DOCKER_ARCHS = $(addprefix common-docker-tag-latest-,$(DOCKER_ARCHS))
 94 | 
 95 | SANITIZED_DOCKER_IMAGE_TAG := $(subst +,-,$(DOCKER_IMAGE_TAG))
 96 | 
 97 | ifeq ($(GOHOSTARCH),amd64)
 98 |         ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux freebsd darwin windows))
 99 |                 # Only supported on amd64
100 |                 test-flags := -race
101 |         endif
102 | endif
103 | 
104 | # This rule is used to forward a target like "build" to "common-build".  This
105 | # allows a new "build" target to be defined in a Makefile which includes this
106 | # one and override "common-build" without override warnings.
107 | %: common-% ;
108 | 
109 | .PHONY: common-all
110 | common-all: precheck style check_license lint yamllint unused build test
111 | 
112 | .PHONY: common-style
113 | common-style:
114 | 	@echo ">> checking code style"
115 | 	@fmtRes=$($(GOFMT) -d $(find . -path ./vendor -prune -o -name '*.go' -print)); \
116 | 	if [ -n "${fmtRes}" ]; then \
117 | 		echo "gofmt checking failed!"; echo "${fmtRes}"; echo; \
118 | 		echo "Please ensure you are using $($(GO) version) for formatting code."; \
119 | 		exit 1; \
120 | 	fi
121 | 
122 | .PHONY: common-check_license
123 | common-check_license:
124 | 	@echo ">> checking license header"
125 | 	@licRes=$(for file in $(find . -type f -iname '*.go' ! -path './vendor/*') ; do \
126 |                awk 'NR<=3' $file | grep -Eq "(Copyright|generated|GENERATED)" || echo $file; \
127 |        done); \
128 |        if [ -n "${licRes}" ]; then \
129 |                echo "license header checking failed:"; echo "${licRes}"; \
130 |                exit 1; \
131 |        fi
132 | 
133 | .PHONY: common-deps
134 | common-deps:
135 | 	@echo ">> getting dependencies"
136 | 	$(GO) mod download
137 | 
138 | .PHONY: update-go-deps
139 | update-go-deps:
140 | 	@echo ">> updating Go dependencies"
141 | 	@for m in $($(GO) list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \
142 | 		$(GO) get $m; \
143 | 	done
144 | 	$(GO) mod tidy
145 | 
146 | .PHONY: common-test-short
147 | common-test-short: $(GOTEST_DIR)
148 | 	@echo ">> running short tests"
149 | 	$(GOTEST) -short $(GOOPTS) $(pkgs)
150 | 
151 | .PHONY: common-test
152 | common-test: $(GOTEST_DIR)
153 | 	@echo ">> running all tests"
154 | 	$(GOTEST) $(test-flags) $(GOOPTS) $(pkgs)
155 | 
156 | $(GOTEST_DIR):
157 | 	@mkdir -p $@
158 | 
159 | .PHONY: common-format
160 | common-format: $(GOLANGCI_LINT)
161 | 	@echo ">> formatting code"
162 | 	$(GO) fmt $(pkgs)
163 | ifdef GOLANGCI_LINT
164 | 	@echo ">> formatting code with golangci-lint"
165 | 	$(GOLANGCI_LINT) fmt $(GOLANGCI_FMT_OPTS)
166 | endif
167 | 
168 | .PHONY: common-vet
169 | common-vet:
170 | 	@echo ">> vetting code"
171 | 	$(GO) vet $(GOOPTS) $(pkgs)
172 | 
173 | .PHONY: common-lint
174 | common-lint: $(GOLANGCI_LINT)
175 | ifdef GOLANGCI_LINT
176 | 	@echo ">> running golangci-lint"
177 | 	$(GOLANGCI_LINT) run $(GOLANGCI_LINT_OPTS) $(pkgs)
178 | endif
179 | 
180 | .PHONY: common-lint-fix
181 | common-lint-fix: $(GOLANGCI_LINT)
182 | ifdef GOLANGCI_LINT
183 | 	@echo ">> running golangci-lint fix"
184 | 	$(GOLANGCI_LINT) run --fix $(GOLANGCI_LINT_OPTS) $(pkgs)
185 | endif
186 | 
187 | .PHONY: common-yamllint
188 | common-yamllint:
189 | 	@echo ">> running yamllint on all YAML files in the repository"
190 | ifeq (, $(shell command -v yamllint 2> /dev/null))
191 | 	@echo "yamllint not installed so skipping"
192 | else
193 | 	yamllint .
194 | endif
195 | 
196 | # For backward-compatibility.
197 | .PHONY: common-staticcheck
198 | common-staticcheck: lint
199 | 
200 | .PHONY: common-unused
201 | common-unused:
202 | 	@echo ">> running check for unused/missing packages in go.mod"
203 | 	$(GO) mod tidy
204 | 	@git diff --exit-code -- go.sum go.mod
205 | 
206 | .PHONY: common-build
207 | common-build: promu
208 | 	@echo ">> building binaries"
209 | 	$(PROMU) build --prefix $(PREFIX) $(PROMU_BINARIES)
210 | 
211 | .PHONY: common-tarball
212 | common-tarball: promu
213 | 	@echo ">> building release tarball"
214 | 	$(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR)
215 | 
216 | .PHONY: common-docker-repo-name
217 | common-docker-repo-name:
218 | 	@echo "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)"
219 | 
220 | .PHONY: common-docker $(BUILD_DOCKER_ARCHS)
221 | common-docker: $(BUILD_DOCKER_ARCHS)
222 | $(BUILD_DOCKER_ARCHS): common-docker-%:
223 | 	docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \
224 | 		-f $(DOCKERFILE_PATH) \
225 | 		--build-arg ARCH="$*" \
226 | 		--build-arg OS="linux" \
227 | 		$(DOCKERBUILD_CONTEXT)
228 | 
229 | .PHONY: common-docker-publish $(PUBLISH_DOCKER_ARCHS)
230 | common-docker-publish: $(PUBLISH_DOCKER_ARCHS)
231 | $(PUBLISH_DOCKER_ARCHS): common-docker-publish-%:
232 | 	docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)"
233 | 
234 | DOCKER_MAJOR_VERSION_TAG = $(firstword $(subst ., ,$(shell cat VERSION)))
235 | .PHONY: common-docker-tag-latest $(TAG_DOCKER_ARCHS)
236 | common-docker-tag-latest: $(TAG_DOCKER_ARCHS)
237 | $(TAG_DOCKER_ARCHS): common-docker-tag-latest-%:
238 | 	docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest"
239 | 	docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)"
240 | 
241 | .PHONY: common-docker-manifest
242 | common-docker-manifest:
243 | 	DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" $(foreach ARCH,$(DOCKER_ARCHS),$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$(ARCH):$(SANITIZED_DOCKER_IMAGE_TAG))
244 | 	DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)"
245 | 
246 | .PHONY: promu
247 | promu: $(PROMU)
248 | 
249 | $(PROMU):
250 | 	$(eval PROMU_TMP := $(shell mktemp -d))
251 | 	curl -s -L $(PROMU_URL) | tar -xvzf - -C $(PROMU_TMP)
252 | 	mkdir -p $(FIRST_GOPATH)/bin
253 | 	cp $(PROMU_TMP)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM)/promu $(FIRST_GOPATH)/bin/promu
254 | 	rm -r $(PROMU_TMP)
255 | 
256 | .PHONY: common-proto
257 | common-proto:
258 | 	@echo ">> generating code from proto files"
259 | 	@./scripts/genproto.sh
260 | 
261 | ifdef GOLANGCI_LINT
262 | $(GOLANGCI_LINT):
263 | 	mkdir -p $(FIRST_GOPATH)/bin
264 | 	curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/$(GOLANGCI_LINT_VERSION)/install.sh \
265 | 		| sed -e '/install -d/d' \
266 | 		| sh -s -- -b $(FIRST_GOPATH)/bin $(GOLANGCI_LINT_VERSION)
267 | endif
268 | 
269 | .PHONY: precheck
270 | precheck::
271 | 
272 | define PRECHECK_COMMAND_template =
273 | precheck:: $(1)_precheck
274 | 
275 | PRECHECK_COMMAND_$(1) ?= $(1) $(strip $(PRECHECK_OPTIONS_$(1)))
276 | .PHONY: $(1)_precheck
277 | $(1)_precheck:
278 | 	@if ! $(PRECHECK_COMMAND_$(1)) 1>/dev/null 2>&1; then \
279 | 		echo "Execution of '$(PRECHECK_COMMAND_$(1))' command failed. Is $(1) installed?"; \
280 | 		exit 1; \
281 | 	fi
282 | endef
283 | 
284 | govulncheck: install-govulncheck
285 | 	govulncheck ./...
286 | 
287 | install-govulncheck:
288 | 	command -v govulncheck > /dev/null || go install golang.org/x/vuln/cmd/govulncheck@latest
289 | 


--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | The Blackbox exporter for blackbox probing metrics
2 | Copyright 2012-2016 The Prometheus Authors
3 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | # Blackbox exporter
  2 | 
  3 | [![CircleCI](https://circleci.com/gh/prometheus/blackbox_exporter/tree/master.svg?style=shield)][circleci]
  4 | [![Docker Repository on Quay](https://quay.io/repository/prometheus/blackbox-exporter/status)][quay]
  5 | [![Docker Pulls](https://img.shields.io/docker/pulls/prom/blackbox-exporter.svg?maxAge=604800)][hub]
  6 | 
  7 | The blackbox exporter allows blackbox probing of endpoints over
  8 | HTTP, HTTPS, DNS, TCP, ICMP and gRPC.
  9 | 
 10 | ## Running this software
 11 | 
 12 | ### From binaries
 13 | 
 14 | Download the most suitable binary from [the releases tab](https://github.com/prometheus/blackbox_exporter/releases)
 15 | 
 16 | Then:
 17 | 
 18 |     ./blackbox_exporter <flags>
 19 | 
 20 | 
 21 | ### Using the docker image
 22 | 
 23 | *Note: You may want to [enable ipv6 in your docker configuration](https://docs.docker.com/v17.09/engine/userguide/networking/default_network/ipv6/)*
 24 | 
 25 |     docker run --rm \
 26 |       -p 9115/tcp \
 27 |       --name blackbox_exporter \
 28 |       -v $(pwd):/config \
 29 |       quay.io/prometheus/blackbox-exporter:latest --config.file=/config/blackbox.yml
 30 | 
 31 | ### Checking the results
 32 | 
 33 | Visiting [http://localhost:9115/probe?target=google.com&module=http_2xx](http://localhost:9115/probe?target=google.com&module=http_2xx)
 34 | will return metrics for a HTTP probe against google.com. The `probe_success`
 35 | metric indicates if the probe succeeded. Adding a `debug=true` parameter
 36 | will return debug information for that probe.
 37 | 
 38 | Metrics concerning the operation of the exporter itself are available at the
 39 | endpoint <http://localhost:9115/metrics>.
 40 | 
 41 | ### TLS and basic authentication
 42 | 
 43 | The Blackbox Exporter supports TLS and basic authentication. This enables better
 44 | control of the various HTTP endpoints.
 45 | 
 46 | To use TLS and/or basic authentication, you need to pass a configuration file
 47 | using the `--web.config.file` parameter. The format of the file is described
 48 | [in the exporter-toolkit repository](https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md).
 49 | 
 50 | Note that the TLS and basic authentication settings affect all HTTP endpoints:
 51 | /metrics for scraping, /probe for probing, and the web UI.
 52 | 
 53 | ### Controlling log level for probe logs
 54 | 
 55 | It is possible to control the level at which probe logs related to a scrape are output as.
 56 | 
 57 | Probe logs default to `debug` level, and can be controlled by the `--log.prober` flag.
 58 | This means that probe scrape logs will not be output unless the level configured for the probe logger via `--log.prober` is >= the level configured for the blackbox_exporter via `--log.level`.
 59 | 
 60 | Sample output demonstrating the use and effect of these flags can be seen below.
 61 | 
 62 | > _Note_
 63 | >
 64 | > All log samples below used the following basic `blackbox.yml` configuration file and contain the probe logs of a single scrape generated by `curl`
 65 | 
 66 | ```bash
 67 | # blackbox.yml
 68 | modules:
 69 |   http_2xx:
 70 |     prober: http
 71 | 
 72 | # generate probe
 73 | curl "http://localhost:9115/probe?target=prometheus.io&module=http_2xx"
 74 | ```
 75 | 
 76 | <details>
 77 | <summary>Example output with `--log.level=info` and `--log.prober=debug` (default)</summary>
 78 | 
 79 | ```bash
 80 | ./blackbox_exporter --config.file ./blackbox.yml --log.level=info --log.prober=debug
 81 | time=2025-05-21T04:10:54.131Z level=INFO source=main.go:88 msg="Starting blackbox_exporter" version="(version=0.26.0, branch=fix/scrape-logger-spam, revision=7df3031feecba82f1a534336979b4e5920f79b72)"
 82 | time=2025-05-21T04:10:54.131Z level=INFO source=main.go:89 msg="(go=go1.24.1, platform=linux/amd64, user=tjhop@contraband, date=20250521-04:00:25, tags=unknown)"
 83 | time=2025-05-21T04:10:54.132Z level=INFO source=main.go:101 msg="Loaded config file"
 84 | time=2025-05-21T04:10:54.133Z level=INFO source=tls_config.go:347 msg="Listening on" address=[::]:9115
 85 | time=2025-05-21T04:10:54.133Z level=INFO source=tls_config.go:350 msg="TLS is disabled." http2=false address=[::]:9115
 86 | ^Ctime=2025-05-21T04:11:03.619Z level=INFO source=main.go:283 msg="Received SIGTERM, exiting gracefully..."
 87 | ```
 88 | </details>
 89 | 
 90 | <details>
 91 | <summary>Example output with `--log.level=info` and `--log.prober=info`</summary>
 92 | 
 93 | ```bash
 94 | ./blackbox_exporter --config.file ./blackbox.yml --log.level=info --log.prober=info
 95 | time=2025-05-21T04:12:09.884Z level=INFO source=main.go:88 msg="Starting blackbox_exporter" version="(version=0.26.0, branch=fix/scrape-logger-spam, revision=7df3031feecba82f1a534336979b4e5920f79b72)"
 96 | time=2025-05-21T04:12:09.884Z level=INFO source=main.go:89 msg="(go=go1.24.1, platform=linux/amd64, user=tjhop@contraband, date=20250521-04:00:25, tags=unknown)"
 97 | time=2025-05-21T04:12:09.884Z level=INFO source=main.go:101 msg="Loaded config file"
 98 | time=2025-05-21T04:12:09.885Z level=INFO source=tls_config.go:347 msg="Listening on" address=[::]:9115
 99 | time=2025-05-21T04:12:09.885Z level=INFO source=tls_config.go:350 msg="TLS is disabled." http2=false address=[::]:9115
100 | time=2025-05-21T04:12:13.827Z level=INFO source=handler.go:194 msg="Beginning probe" module=http_2xx target=prometheus.io probe=http timeout_seconds=119.5
101 | time=2025-05-21T04:12:13.827Z level=INFO source=handler.go:194 msg="Resolving target address" module=http_2xx target=prometheus.io target=prometheus.io ip_protocol=ip4
102 | time=2025-05-21T04:12:13.829Z level=INFO source=handler.go:194 msg="Resolved target address" module=http_2xx target=prometheus.io target=prometheus.io ip=172.67.201.240
103 | time=2025-05-21T04:12:13.829Z level=INFO source=handler.go:194 msg="Making HTTP request" module=http_2xx target=prometheus.io url=http://172.67.201.240 host=prometheus.io
104 | time=2025-05-21T04:12:13.860Z level=INFO source=handler.go:194 msg="Received redirect" module=http_2xx target=prometheus.io location=https://prometheus.io/
105 | time=2025-05-21T04:12:13.860Z level=INFO source=handler.go:194 msg="Making HTTP request" module=http_2xx target=prometheus.io url=https://prometheus.io/ host=""
106 | time=2025-05-21T04:12:13.860Z level=INFO source=handler.go:194 msg="Address does not match first address, not sending TLS ServerName" module=http_2xx target=prometheus.io first=172.67.201.240 address=prometheus.io
107 | time=2025-05-21T04:12:13.974Z level=INFO source=handler.go:194 msg="Received HTTP response" module=http_2xx target=prometheus.io status_code=200
108 | time=2025-05-21T04:12:13.974Z level=INFO source=handler.go:194 msg="Response timings for roundtrip" module=http_2xx target=prometheus.io roundtrip=0 start=2025-05-21T00:12:13.829-04:00 dnsDone=2025-05-21T00:12:13.829-04:00 connectDone=2025-05-21T00:12:13.839-04:00 gotConn=2025-05-21T00:12:13.839-04:00 responseStart=2025-05-21T00:12:13.860-04:00 tlsStart=0001-01-01T00:00:00.000Z tlsDone=0001-01-01T00:00:00.000Z end=0001-01-01T00:00:00.000Z
109 | time=2025-05-21T04:12:13.974Z level=INFO source=handler.go:194 msg="Response timings for roundtrip" module=http_2xx target=prometheus.io roundtrip=1 start=2025-05-21T00:12:13.860-04:00 dnsDone=2025-05-21T00:12:13.861-04:00 connectDone=2025-05-21T00:12:13.869-04:00 gotConn=2025-05-21T00:12:13.925-04:00 responseStart=2025-05-21T00:12:13.974-04:00 tlsStart=2025-05-21T00:12:13.869-04:00 tlsDone=2025-05-21T00:12:13.925-04:00 end=2025-05-21T00:12:13.974-04:00
110 | time=2025-05-21T04:12:13.974Z level=INFO source=handler.go:194 msg="Probe succeeded" module=http_2xx target=prometheus.io duration_seconds=0.14708839
111 | ^Ctime=2025-05-21T04:12:17.818Z level=INFO source=main.go:283 msg="Received SIGTERM, exiting gracefully..."
112 | ```
113 | </details>
114 | 
115 | <details>
116 | <summary>Example output with `--log.level=debug` and `--log.prober=info`</summary>
117 | 
118 | ```bash
119 | ./blackbox_exporter --config.file ./blackbox.yml --log.level=debug --log.prober=info 
120 | time=2025-05-21T04:13:18.497Z level=INFO source=main.go:88 msg="Starting blackbox_exporter" version="(version=0.26.0, branch=fix/scrape-logger-spam, revision=7df3031feecba82f1a534336979b4e5920f79b72)"
121 | time=2025-05-21T04:13:18.497Z level=INFO source=main.go:89 msg="(go=go1.24.1, platform=linux/amd64, user=tjhop@contraband, date=20250521-04:00:25, tags=unknown)"
122 | time=2025-05-21T04:13:18.497Z level=INFO source=main.go:101 msg="Loaded config file"
123 | time=2025-05-21T04:13:18.498Z level=DEBUG source=main.go:116 msg=http://contraband:9115
124 | time=2025-05-21T04:13:18.498Z level=DEBUG source=main.go:130 msg=/
125 | time=2025-05-21T04:13:18.498Z level=INFO source=tls_config.go:347 msg="Listening on" address=[::]:9115
126 | time=2025-05-21T04:13:18.498Z level=INFO source=tls_config.go:350 msg="TLS is disabled." http2=false address=[::]:9115
127 | time=2025-05-21T04:13:23.169Z level=INFO source=handler.go:194 msg="Beginning probe" module=http_2xx target=prometheus.io probe=http timeout_seconds=119.5
128 | time=2025-05-21T04:13:23.169Z level=INFO source=handler.go:194 msg="Resolving target address" module=http_2xx target=prometheus.io target=prometheus.io ip_protocol=ip4
129 | time=2025-05-21T04:13:23.170Z level=INFO source=handler.go:194 msg="Resolved target address" module=http_2xx target=prometheus.io target=prometheus.io ip=104.21.60.220
130 | time=2025-05-21T04:13:23.170Z level=INFO source=handler.go:194 msg="Making HTTP request" module=http_2xx target=prometheus.io url=http://104.21.60.220 host=prometheus.io
131 | time=2025-05-21T04:13:23.202Z level=INFO source=handler.go:194 msg="Received redirect" module=http_2xx target=prometheus.io location=https://prometheus.io/
132 | time=2025-05-21T04:13:23.202Z level=INFO source=handler.go:194 msg="Making HTTP request" module=http_2xx target=prometheus.io url=https://prometheus.io/ host=""
133 | time=2025-05-21T04:13:23.202Z level=INFO source=handler.go:194 msg="Address does not match first address, not sending TLS ServerName" module=http_2xx target=prometheus.io first=104.21.60.220 address=prometheus.io
134 | time=2025-05-21T04:13:23.316Z level=INFO source=handler.go:194 msg="Received HTTP response" module=http_2xx target=prometheus.io status_code=200
135 | time=2025-05-21T04:13:23.319Z level=INFO source=handler.go:194 msg="Response timings for roundtrip" module=http_2xx target=prometheus.io roundtrip=0 start=2025-05-21T00:13:23.171-04:00 dnsDone=2025-05-21T00:13:23.171-04:00 connectDone=2025-05-21T00:13:23.181-04:00 gotConn=2025-05-21T00:13:23.181-04:00 responseStart=2025-05-21T00:13:23.201-04:00 tlsStart=0001-01-01T00:00:00.000Z tlsDone=0001-01-01T00:00:00.000Z end=0001-01-01T00:00:00.000Z
136 | time=2025-05-21T04:13:23.319Z level=INFO source=handler.go:194 msg="Response timings for roundtrip" module=http_2xx target=prometheus.io roundtrip=1 start=2025-05-21T00:13:23.202-04:00 dnsDone=2025-05-21T00:13:23.203-04:00 connectDone=2025-05-21T00:13:23.212-04:00 gotConn=2025-05-21T00:13:23.268-04:00 responseStart=2025-05-21T00:13:23.316-04:00 tlsStart=2025-05-21T00:13:23.212-04:00 tlsDone=2025-05-21T00:13:23.268-04:00 end=2025-05-21T00:13:23.319-04:00
137 | time=2025-05-21T04:13:23.319Z level=INFO source=handler.go:194 msg="Probe succeeded" module=http_2xx target=prometheus.io duration_seconds=0.150580389
138 | ^Ctime=2025-05-21T04:13:27.945Z level=INFO source=main.go:283 msg="Received SIGTERM, exiting gracefully..."
139 | ```
140 | </details>
141 | 
142 | 
143 | <details>
144 | <summary>Example output with `--log.level=debug` and `--log.prober=debug`</summary>
145 | 
146 | ```bash
147 | ./blackbox_exporter --config.file ./blackbox.yml --log.level=debug --log.prober=debug
148 | time=2025-05-21T04:14:55.621Z level=INFO source=main.go:88 msg="Starting blackbox_exporter" version="(version=0.26.0, branch=fix/scrape-logger-spam, revision=7df3031feecba82f1a534336979b4e5920f79b72)"
149 | time=2025-05-21T04:14:55.621Z level=INFO source=main.go:89 msg="(go=go1.24.1, platform=linux/amd64, user=tjhop@contraband, date=20250521-04:00:25, tags=unknown)"
150 | time=2025-05-21T04:14:55.622Z level=INFO source=main.go:101 msg="Loaded config file"
151 | time=2025-05-21T04:14:55.622Z level=DEBUG source=main.go:116 msg=http://contraband:9115
152 | time=2025-05-21T04:14:55.622Z level=DEBUG source=main.go:130 msg=/
153 | time=2025-05-21T04:14:55.623Z level=INFO source=tls_config.go:347 msg="Listening on" address=[::]:9115
154 | time=2025-05-21T04:14:55.623Z level=INFO source=tls_config.go:350 msg="TLS is disabled." http2=false address=[::]:9115
155 | time=2025-05-21T04:15:03.048Z level=DEBUG source=handler.go:194 msg="Beginning probe" module=http_2xx target=prometheus.io probe=http timeout_seconds=119.5
156 | time=2025-05-21T04:15:03.049Z level=DEBUG source=handler.go:194 msg="Resolving target address" module=http_2xx target=prometheus.io target=prometheus.io ip_protocol=ip4
157 | time=2025-05-21T04:15:03.050Z level=DEBUG source=handler.go:194 msg="Resolved target address" module=http_2xx target=prometheus.io target=prometheus.io ip=172.67.201.240
158 | time=2025-05-21T04:15:03.050Z level=DEBUG source=handler.go:194 msg="Making HTTP request" module=http_2xx target=prometheus.io url=http://172.67.201.240 host=prometheus.io
159 | time=2025-05-21T04:15:03.089Z level=DEBUG source=handler.go:194 msg="Received redirect" module=http_2xx target=prometheus.io location=https://prometheus.io/
160 | time=2025-05-21T04:15:03.089Z level=DEBUG source=handler.go:194 msg="Making HTTP request" module=http_2xx target=prometheus.io url=https://prometheus.io/ host=""
161 | time=2025-05-21T04:15:03.089Z level=DEBUG source=handler.go:194 msg="Address does not match first address, not sending TLS ServerName" module=http_2xx target=prometheus.io first=172.67.201.240 address=prometheus.io
162 | time=2025-05-21T04:15:03.211Z level=DEBUG source=handler.go:194 msg="Received HTTP response" module=http_2xx target=prometheus.io status_code=200
163 | time=2025-05-21T04:15:03.212Z level=DEBUG source=handler.go:194 msg="Response timings for roundtrip" module=http_2xx target=prometheus.io roundtrip=0 start=2025-05-21T00:15:03.050-04:00 dnsDone=2025-05-21T00:15:03.050-04:00 connectDone=2025-05-21T00:15:03.061-04:00 gotConn=2025-05-21T00:15:03.061-04:00 responseStart=2025-05-21T00:15:03.089-04:00 tlsStart=0001-01-01T00:00:00.000Z tlsDone=0001-01-01T00:00:00.000Z end=0001-01-01T00:00:00.000Z
164 | time=2025-05-21T04:15:03.212Z level=DEBUG source=handler.go:194 msg="Response timings for roundtrip" module=http_2xx target=prometheus.io roundtrip=1 start=2025-05-21T00:15:03.089-04:00 dnsDone=2025-05-21T00:15:03.090-04:00 connectDone=2025-05-21T00:15:03.102-04:00 gotConn=2025-05-21T00:15:03.163-04:00 responseStart=2025-05-21T00:15:03.211-04:00 tlsStart=2025-05-21T00:15:03.102-04:00 tlsDone=2025-05-21T00:15:03.163-04:00 end=2025-05-21T00:15:03.212-04:00
165 | time=2025-05-21T04:15:03.212Z level=DEBUG source=handler.go:194 msg="Probe succeeded" module=http_2xx target=prometheus.io duration_seconds=0.163695815
166 | ^Ctime=2025-05-21T04:15:07.862Z level=INFO source=main.go:283 msg="Received SIGTERM, exiting gracefully..."
167 | ```
168 | </details>
169 | 
170 | ## Building the software
171 | 
172 | ### Local Build
173 | 
174 |     make
175 | 
176 | 
177 | ### Building with Docker
178 | 
179 | After a successful local build:
180 | 
181 |     docker build -t blackbox_exporter .
182 | 
183 | ## [Configuration](CONFIGURATION.md)
184 | 
185 | Blackbox exporter is configured via a [configuration file](CONFIGURATION.md) and command-line flags (such as what configuration file to load, what port to listen on, and the logging format and level).
186 | 
187 | Blackbox exporter can reload its configuration file at runtime. If the new configuration is not well-formed, the changes will not be applied.
188 | A configuration reload is triggered by sending a `SIGHUP` to the Blackbox exporter process or by sending a HTTP POST request to the `/-/reload` endpoint.
189 | 
190 | To view all available command-line flags, run `./blackbox_exporter -h`.
191 | 
192 | To specify which [configuration file](CONFIGURATION.md) to load, use the `--config.file` flag.
193 | 
194 | Additionally, an [example configuration](example.yml) is also available.
195 | 
196 | HTTP, HTTPS (via the `http` prober), DNS, TCP socket, ICMP and gRPC (see permissions section) are currently supported.
197 | Additional modules can be defined to meet your needs.
198 | 
199 | The timeout of each probe is automatically determined from the `scrape_timeout` in the [Prometheus config](https://prometheus.io/docs/operating/configuration/#configuration-file), slightly reduced to allow for network delays. 
200 | This can be further limited by the `timeout` in the Blackbox exporter config file. If neither is specified, it defaults to 120 seconds.
201 | 
202 | ## Prometheus Configuration
203 | 
204 | Blackbox exporter implements the multi-target exporter pattern, so we advice
205 | to read the guide [Understanding and using the multi-target exporter pattern
206 | ](https://prometheus.io/docs/guides/multi-target-exporter/) to get the general
207 | idea about the configuration.
208 | 
209 | The blackbox exporter needs to be passed the target as a parameter, this can be
210 | done with relabelling.
211 | 
212 | Example config:
213 | ```yml
214 | scrape_configs:
215 |   - job_name: 'blackbox'
216 |     metrics_path: /probe
217 |     params:
218 |       module: [http_2xx]  # Look for a HTTP 200 response.
219 |     static_configs:
220 |       - targets:
221 |         - http://prometheus.io    # Target to probe with http.
222 |         - https://prometheus.io   # Target to probe with https.
223 |         - http://example.com:8080 # Target to probe with http on port 8080.
224 |     relabel_configs:
225 |       - source_labels: [__address__]
226 |         target_label: __param_target
227 |       - source_labels: [__param_target]
228 |         target_label: instance
229 |       - target_label: __address__
230 |         replacement: 127.0.0.1:9115  # The blackbox exporter's real hostname:port.
231 |   - job_name: 'blackbox_exporter'  # collect blackbox exporter's operational metrics.
232 |     static_configs:
233 |       - targets: ['127.0.0.1:9115']
234 | ```
235 | 
236 | HTTP probes can accept an additional `hostname` parameter that will set `Host` header and TLS SNI. This can be especially useful with `dns_sd_config`:
237 | ```yaml
238 | scrape_configs:
239 |   - job_name: blackbox_all
240 |     metrics_path: /probe
241 |     params:
242 |       module: [ http_2xx ]  # Look for a HTTP 200 response.
243 |     dns_sd_configs:
244 |       - names:
245 |           - example.com
246 |           - prometheus.io
247 |         type: A
248 |         port: 443
249 |     relabel_configs:
250 |       - source_labels: [__address__]
251 |         target_label: __param_target
252 |         replacement: https://$1/  # Make probe URL be like https://1.2.3.4:443/
253 |       - source_labels: [__param_target]
254 |         target_label: instance
255 |       - target_label: __address__
256 |         replacement: 127.0.0.1:9115  # The blackbox exporter's real hostname:port.
257 |       - source_labels: [__meta_dns_name]
258 |         target_label: __param_hostname  # Make domain name become 'Host' header for probe requests
259 |       - source_labels: [__meta_dns_name]
260 |         target_label: vhost  # and store it in 'vhost' label
261 | ```
262 | 
263 | ## Permissions
264 | 
265 | The ICMP probe requires elevated privileges to function:
266 | 
267 | * *Windows*: Administrator privileges are required.
268 | * *Linux*: either a user with a group within `net.ipv4.ping_group_range`, the
269 |   `CAP_NET_RAW` capability or the root user is required.
270 |   * Your distribution may configure `net.ipv4.ping_group_range` by default in
271 |     `/etc/sysctl.conf` or similar. If not you can set
272 |     `net.ipv4.ping_group_range = 0  2147483647` to allow any user the ability
273 |     to use ping.
274 |   * Alternatively the capability can be set by executing `setcap cap_net_raw+ep
275 |     blackbox_exporter`
276 | * *BSD*: root user is required.
277 | * *OS X*: No additional privileges are needed.
278 | 
279 | [circleci]: https://circleci.com/gh/prometheus/blackbox_exporter
280 | [hub]: https://hub.docker.com/r/prom/blackbox-exporter/
281 | [quay]: https://quay.io/repository/prometheus/blackbox-exporter
282 | 


--------------------------------------------------------------------------------
/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 | <https://prometheus.io/docs/operating/security/>
7 | 


--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
1 | 0.27.0
2 | 


--------------------------------------------------------------------------------
/blackbox.yml:
--------------------------------------------------------------------------------
 1 | modules:
 2 |   http_2xx:
 3 |     prober: http
 4 |     http:
 5 |       preferred_ip_protocol: "ip4"
 6 |   http_post_2xx:
 7 |     prober: http
 8 |     http:
 9 |       method: POST
10 |   tcp_connect:
11 |     prober: tcp
12 |   pop3s_banner:
13 |     prober: tcp
14 |     tcp:
15 |       query_response:
16 |       - expect: "^+OK"
17 |       tls: true
18 |       tls_config:
19 |         insecure_skip_verify: false
20 |   grpc:
21 |     prober: grpc
22 |     grpc:
23 |       tls: true
24 |       preferred_ip_protocol: "ip4"
25 |   grpc_plain:
26 |     prober: grpc
27 |     grpc:
28 |       tls: false
29 |       service: "service1"
30 |   ssh_banner:
31 |     prober: tcp
32 |     tcp:
33 |       query_response:
34 |       - expect: "^SSH-2.0-"
35 |       - send: "SSH-2.0-blackbox-ssh-check"
36 |   ssh_banner_extract:
37 |     prober: tcp
38 |     timeout: 5s
39 |     tcp:
40 |       query_response:
41 |       - expect: "^SSH-2.0-([^ -]+)(?: (.*))?
quot;
42 |         labels:
43 |         - name: ssh_version
44 |           value: "${1}"
45 |         - name: ssh_comments
46 |           value: "${2}"
47 |   irc_banner:
48 |     prober: tcp
49 |     tcp:
50 |       query_response:
51 |       - send: "NICK prober"
52 |       - send: "USER prober prober prober :prober"
53 |       - expect: "PING :([^ ]+)"
54 |         send: "PONG ${1}"
55 |       - expect: "^:[^ ]+ 001"
56 |   icmp:
57 |     prober: icmp
58 |   icmp_ttl5:
59 |     prober: icmp
60 |     timeout: 5s
61 |     icmp:
62 |       ttl: 5
63 | 


--------------------------------------------------------------------------------
/config/config_test.go:
--------------------------------------------------------------------------------
  1 | // Copyright 2016 The Prometheus Authors
  2 | // Licensed under the Apache License, Version 2.0 (the "License");
  3 | // you may not use this file except in compliance with the License.
  4 | // You may obtain a copy of the License at
  5 | //
  6 | // http://www.apache.org/licenses/LICENSE-2.0
  7 | //
  8 | // Unless required by applicable law or agreed to in writing, software
  9 | // distributed under the License is distributed on an "AS IS" BASIS,
 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 11 | // See the License for the specific language governing permissions and
 12 | // limitations under the License.
 13 | 
 14 | package config
 15 | 
 16 | import (
 17 | 	"strings"
 18 | 	"testing"
 19 | 
 20 | 	"github.com/prometheus/client_golang/prometheus"
 21 | 	yaml "gopkg.in/yaml.v3"
 22 | )
 23 | 
 24 | func TestLoadConfig(t *testing.T) {
 25 | 	sc := NewSafeConfig(prometheus.NewRegistry())
 26 | 
 27 | 	err := sc.ReloadConfig("testdata/blackbox-good.yml", nil)
 28 | 	if err != nil {
 29 | 		t.Errorf("Error loading config %v: %v", "blackbox.yml", err)
 30 | 	}
 31 | }
 32 | 
 33 | func TestLoadBadConfigs(t *testing.T) {
 34 | 	sc := NewSafeConfig(prometheus.NewRegistry())
 35 | 	tests := []struct {
 36 | 		input string
 37 | 		want  string
 38 | 	}{
 39 | 		{
 40 | 			input: "testdata/blackbox-bad.yml",
 41 | 			want:  "error parsing config file: yaml: unmarshal errors:\n  line 50: field invalid_extra_field not found in type config.plain",
 42 | 		},
 43 | 		{
 44 | 			input: "testdata/blackbox-bad2.yml",
 45 | 			want:  "error parsing config file: at most one of bearer_token & bearer_token_file must be configured",
 46 | 		},
 47 | 		{
 48 | 			input: "testdata/invalid-dns-module.yml",
 49 | 			want:  "error parsing config file: query name must be set for DNS module",
 50 | 		},
 51 | 		{
 52 | 			input: "testdata/invalid-dns-class.yml",
 53 | 			want:  "error parsing config file: query class 'X' is not valid",
 54 | 		},
 55 | 		{
 56 | 			input: "testdata/invalid-dns-type.yml",
 57 | 			want:  "error parsing config file: query type 'X' is not valid",
 58 | 		},
 59 | 		{
 60 | 			input: "testdata/invalid-http-header-match.yml",
 61 | 			want:  "error parsing config file: regexp must be set for HTTP header matchers",
 62 | 		},
 63 | 		{
 64 | 			input: "testdata/invalid-http-body-match-regexp.yml",
 65 | 			want:  `error parsing config file: "Could not compile regular expression" regexp=":["`,
 66 | 		},
 67 | 		{
 68 | 			input: "testdata/invalid-http-body-not-match-regexp.yml",
 69 | 			want:  `error parsing config file: "Could not compile regular expression" regexp=":["`,
 70 | 		},
 71 | 		{
 72 | 			input: "testdata/invalid-http-header-match-regexp.yml",
 73 | 			want:  `error parsing config file: "Could not compile regular expression" regexp=":["`,
 74 | 		},
 75 | 		{
 76 | 			input: "testdata/invalid-http-compression-mismatch.yml",
 77 | 			want:  `error parsing config file: invalid configuration "Accept-Encoding: deflate", "compression: gzip"`,
 78 | 		},
 79 | 		{
 80 | 			input: "testdata/invalid-http-compression-mismatch-special-case.yml",
 81 | 			want:  `error parsing config file: invalid configuration "accEpt-enCoding: deflate", "compression: gzip"`,
 82 | 		},
 83 | 		{
 84 | 			input: "testdata/invalid-http-request-compression-reject-all-encodings.yml",
 85 | 			want:  `error parsing config file: invalid configuration "Accept-Encoding: *;q=0.0", "compression: gzip"`,
 86 | 		},
 87 | 		{
 88 | 			input: "testdata/invalid-icmp-ttl.yml",
 89 | 			want:  "error parsing config file: \"ttl\" cannot be negative",
 90 | 		},
 91 | 		{
 92 | 			input: "testdata/invalid-icmp-ttl-overflow.yml",
 93 | 			want:  "error parsing config file: \"ttl\" cannot exceed 255",
 94 | 		},
 95 | 		{
 96 | 			input: "testdata/invalid-tcp-query-response-regexp.yml",
 97 | 			want:  `error parsing config file: "Could not compile regular expression" regexp=":["`,
 98 | 		},
 99 | 		{
100 | 			input: "testdata/invalid-http-body-config.yml",
101 | 			want:  `error parsing config file: setting body and body_file both are not allowed`,
102 | 		},
103 | 	}
104 | 	for _, test := range tests {
105 | 		t.Run(test.input, func(t *testing.T) {
106 | 			got := sc.ReloadConfig(test.input, nil)
107 | 			if got == nil || got.Error() != test.want {
108 | 				t.Fatalf("ReloadConfig(%q) = %v; want %q", test.input, got, test.want)
109 | 			}
110 | 		})
111 | 	}
112 | }
113 | 
114 | func TestHideConfigSecrets(t *testing.T) {
115 | 	sc := NewSafeConfig(prometheus.NewRegistry())
116 | 
117 | 	err := sc.ReloadConfig("testdata/blackbox-good.yml", nil)
118 | 	if err != nil {
119 | 		t.Errorf("Error loading config %v: %v", "testdata/blackbox-good.yml", err)
120 | 	}
121 | 
122 | 	// String method must not reveal authentication credentials.
123 | 	sc.RLock()
124 | 	c, err := yaml.Marshal(sc.C)
125 | 	sc.RUnlock()
126 | 	if err != nil {
127 | 		t.Errorf("Error marshalling config: %v", err)
128 | 	}
129 | 	if strings.Contains(string(c), "mysecret") {
130 | 		t.Fatal("config's String method reveals authentication credentials.")
131 | 	}
132 | }
133 | 
134 | func TestIsEncodingAcceptable(t *testing.T) {
135 | 	testcases := map[string]struct {
136 | 		input          string
137 | 		acceptEncoding string
138 | 		expected       bool
139 | 	}{
140 | 		"empty compression": {
141 | 			input:          "",
142 | 			acceptEncoding: "gzip",
143 | 			expected:       true,
144 | 		},
145 | 		"trivial": {
146 | 			input:          "gzip",
147 | 			acceptEncoding: "gzip",
148 | 			expected:       true,
149 | 		},
150 | 		"trivial, quality": {
151 | 			input:          "gzip",
152 | 			acceptEncoding: "gzip;q=1.0",
153 | 			expected:       true,
154 | 		},
155 | 		"first": {
156 | 			input:          "gzip",
157 | 			acceptEncoding: "gzip, compress",
158 | 			expected:       true,
159 | 		},
160 | 		"second": {
161 | 			input:          "gzip",
162 | 			acceptEncoding: "compress, gzip",
163 | 			expected:       true,
164 | 		},
165 | 		"missing": {
166 | 			input:          "br",
167 | 			acceptEncoding: "gzip, compress",
168 | 			expected:       false,
169 | 		},
170 | 		"*": {
171 | 			input:          "br",
172 | 			acceptEncoding: "gzip, compress, *",
173 | 			expected:       true,
174 | 		},
175 | 		"* with quality": {
176 | 			input:          "br",
177 | 			acceptEncoding: "gzip, compress, *;q=0.1",
178 | 			expected:       true,
179 | 		},
180 | 		"rejected": {
181 | 			input:          "br",
182 | 			acceptEncoding: "gzip, compress, br;q=0.0",
183 | 			expected:       false,
184 | 		},
185 | 		"rejected *": {
186 | 			input:          "br",
187 | 			acceptEncoding: "gzip, compress, *;q=0.0",
188 | 			expected:       false,
189 | 		},
190 | 		"complex": {
191 | 			input:          "br",
192 | 			acceptEncoding: "gzip;q=1.0, compress;q=0.5, br;q=0.1, *;q=0.0",
193 | 			expected:       true,
194 | 		},
195 | 		"complex out of order": {
196 | 			input:          "br",
197 | 			acceptEncoding: "*;q=0.0, compress;q=0.5, br;q=0.1, gzip;q=1.0",
198 | 			expected:       true,
199 | 		},
200 | 		"complex with extra blanks": {
201 | 			input:          "br",
202 | 			acceptEncoding: " gzip;q=1.0, compress; q=0.5, br;q=0.1, *; q=0.0 ",
203 | 			expected:       true,
204 | 		},
205 | 	}
206 | 
207 | 	for name, tc := range testcases {
208 | 		t.Run(name, func(t *testing.T) {
209 | 			actual := isCompressionAcceptEncodingValid(tc.input, tc.acceptEncoding)
210 | 			if actual != tc.expected {
211 | 				t.Errorf("Unexpected result: input=%q acceptEncoding=%q expected=%t actual=%t", tc.input, tc.acceptEncoding, tc.expected, actual)
212 | 			}
213 | 		})
214 | 	}
215 | }
216 | 
217 | func TestNewCELProgram(t *testing.T) {
218 | 	tests := []struct {
219 | 		name    string
220 | 		expr    string
221 | 		wantErr bool
222 | 	}{
223 | 		{
224 | 			name:    "valid expression",
225 | 			expr:    "body.foo == 'bar'",
226 | 			wantErr: false,
227 | 		},
228 | 		{
229 | 			name:    "invalid expression",
230 | 			expr:    "foo.bar",
231 | 			wantErr: true,
232 | 		},
233 | 		{
234 | 			name:    "empty expression",
235 | 			expr:    "",
236 | 			wantErr: true,
237 | 		},
238 | 	}
239 | 	for _, tt := range tests {
240 | 		t.Run(tt.name, func(t *testing.T) {
241 | 			_, err := NewCELProgram(tt.expr)
242 | 			if (err != nil) != tt.wantErr {
243 | 				t.Errorf("NewCELProgram() error = %v, wantErr %v", err, tt.wantErr)
244 | 				return
245 | 			}
246 | 		})
247 | 	}
248 | }
249 | 


--------------------------------------------------------------------------------
/config/testdata/blackbox-bad.yml:
--------------------------------------------------------------------------------
 1 | modules:
 2 |   http_2xx:
 3 |     prober: http
 4 |     timeout: 5s
 5 |     http:
 6 |   http_post_2xx:
 7 |     prober: http
 8 |     timeout: 5s
 9 |     http:
10 |       method: POST
11 |   tcp_connect:
12 |     prober: tcp
13 |     timeout: 5s
14 |   pop3s_banner:
15 |     prober: tcp
16 |     tcp:
17 |       query_response:
18 |       - expect: "^+OK"
19 |       tls: true
20 |       tls_config:
21 |         insecure_skip_verify: false
22 |   ssh_banner:
23 |     prober: tcp
24 |     timeout: 5s
25 |     tcp:
26 |       query_response:
27 |       - expect: "^SSH-2.0-"
28 |   irc_banner:
29 |     prober: tcp
30 |     timeout: 5s
31 |     tcp:
32 |       query_response:
33 |       - send: "NICK prober"
34 |       - send: "USER prober prober prober :prober"
35 |       - expect: "PING :([^ ]+)"
36 |         send: "PONG ${1}"
37 |       - expect: "^:[^ ]+ 001"
38 |   icmp_test:
39 |     prober: icmp
40 |     timeout: 5s
41 |     icmp:
42 |       preferred_ip_protocol: ip4
43 |   dns_test:
44 |     prober: dns
45 |     timeout: 5s
46 |     dns:
47 |       preferred_ip_protocol: ip6
48 |       validate_answer_rrs:
49 |         fail_if_matches_regexp: [test]
50 |       invalid_extra_field: value
51 | 


--------------------------------------------------------------------------------
/config/testdata/blackbox-bad2.yml:
--------------------------------------------------------------------------------
 1 | modules:
 2 |   http_post_2xx:
 3 |     prober: http
 4 |     timeout: 5s
 5 |     http:
 6 |       method: POST
 7 |       bearer_token: foo
 8 |       bearer_token_file: foo
 9 |       basic_auth:
10 |         username: "username"
11 |         password: "mysecret"
12 | 


--------------------------------------------------------------------------------
/config/testdata/blackbox-good.yml:
--------------------------------------------------------------------------------
 1 | modules:
 2 |   http_2xx:
 3 |     prober: http
 4 |     timeout: 5s
 5 |     http:
 6 |   http_post_2xx:
 7 |     prober: http
 8 |     timeout: 5s
 9 |     http:
10 |       method: POST
11 |       basic_auth:
12 |         username: "username"
13 |         password: "mysecret"
14 |       body_size_limit: 1MB
15 |   tcp_connect:
16 |     prober: tcp
17 |     timeout: 5s
18 |   pop3s_banner:
19 |     prober: tcp
20 |     tcp:
21 |       query_response:
22 |       - expect: "^+OK"
23 |       tls: true
24 |       tls_config:
25 |         insecure_skip_verify: false
26 |   ssh_banner:
27 |     prober: tcp
28 |     timeout: 5s
29 |     tcp:
30 |       query_response:
31 |       - expect: "^SSH-2.0-"
32 |   smtp_starttls:
33 |     prober: tcp
34 |     timeout: 5s
35 |     tcp:
36 |       query_response:
37 |       - expect: "^220 "
38 |       - send: "EHLO prober\r"
39 |       - expect: "^250-STARTTLS"
40 |       - send: "STARTTLS\r"
41 |       - expect: "^220"
42 |       - starttls: true
43 |       - send: "EHLO prober\r"
44 |       - expect: "^250-AUTH"
45 |       - send: "QUIT\r"
46 |   irc_banner:
47 |     prober: tcp
48 |     timeout: 5s
49 |     tcp:
50 |       query_response:
51 |       - send: "NICK prober"
52 |       - send: "USER prober prober prober :prober"
53 |       - expect: "PING :([^ ]+)"
54 |         send: "PONG ${1}"
55 |       - expect: "^:[^ ]+ 001"
56 |   icmp_test:
57 |     prober: icmp
58 |     timeout: 5s
59 |     icmp:
60 |       preferred_ip_protocol: ip4
61 |   dns_test:
62 |     prober: dns
63 |     timeout: 5s
64 |     dns:
65 |       query_name: example.com
66 |       preferred_ip_protocol: ip4
67 |       ip_protocol_fallback: false
68 |       validate_answer_rrs:
69 |         fail_if_matches_regexp: [test]
70 |   http_header_match_origin:
71 |     prober: http
72 |     timeout: 5s
73 |     http:
74 |       method: GET
75 |       headers:
76 |         Origin: example.com
77 |       fail_if_header_not_matches:
78 |       - header: Access-Control-Allow-Origin
79 |         regexp: '(\*|example\.com)'
80 |         allow_missing: false
81 | 


--------------------------------------------------------------------------------
/config/testdata/invalid-dns-class.yml:
--------------------------------------------------------------------------------
1 | modules:
2 |   dns_test:
3 |     prober: dns
4 |     timeout: 5s
5 |     dns:
6 |       query_name: example.com
7 |       query_class: X
8 |       query_type: A
9 | 


--------------------------------------------------------------------------------
/config/testdata/invalid-dns-module.yml:
--------------------------------------------------------------------------------
1 | modules:
2 |   dns_test:
3 |     prober: dns
4 |     timeout: 5s
5 |     dns:
6 |       preferred_ip_protocol: ip6
7 |       validate_answer_rrs:
8 |         fail_if_matches_regexp: [test]
9 | 


--------------------------------------------------------------------------------
/config/testdata/invalid-dns-type.yml:
--------------------------------------------------------------------------------
1 | modules:
2 |   dns_test:
3 |     prober: dns
4 |     timeout: 5s
5 |     dns:
6 |       query_name: example.com
7 |       query_class: CH
8 |       query_type: X
9 | 


--------------------------------------------------------------------------------
/config/testdata/invalid-http-body-config.yml:
--------------------------------------------------------------------------------
1 | modules:
2 |   http_test:
3 |     prober: http
4 |     timeout: 5s
5 |     http:
6 |       body: "Test body"
7 |       body_file: "test_body.txt"
8 | 


--------------------------------------------------------------------------------
/config/testdata/invalid-http-body-match-regexp.yml:
--------------------------------------------------------------------------------
1 | modules:
2 |   http_headers:
3 |     prober: http
4 |     timeout: 5s
5 |     http:
6 |       fail_if_body_matches_regexp:
7 |         - ':['
8 | 


--------------------------------------------------------------------------------
/config/testdata/invalid-http-body-not-match-regexp.yml:
--------------------------------------------------------------------------------
1 | modules:
2 |   http_headers:
3 |     prober: http
4 |     timeout: 5s
5 |     http:
6 |       fail_if_body_not_matches_regexp:
7 |         - ':['
8 | 


--------------------------------------------------------------------------------
/config/testdata/invalid-http-compression-mismatch-special-case.yml:
--------------------------------------------------------------------------------
1 | modules:
2 |   http_headers:
3 |     prober: http
4 |     timeout: 5s
5 |     http:
6 |       compression: gzip
7 |       headers:
8 |         "accEpt-enCoding": "deflate"
9 | 


--------------------------------------------------------------------------------
/config/testdata/invalid-http-compression-mismatch.yml:
--------------------------------------------------------------------------------
1 | modules:
2 |   http_headers:
3 |     prober: http
4 |     timeout: 5s
5 |     http:
6 |       compression: gzip
7 |       headers:
8 |         "Accept-Encoding": "deflate"
9 | 


--------------------------------------------------------------------------------
/config/testdata/invalid-http-header-match-regexp.yml:
--------------------------------------------------------------------------------
 1 | modules:
 2 |   http_headers:
 3 |     prober: http
 4 |     timeout: 5s
 5 |     http:
 6 |       fail_if_header_not_matches:
 7 |         - header: Access-Control-Allow-Origin
 8 |           allow_missing: false
 9 |           regexp: ':['
10 | 


--------------------------------------------------------------------------------
/config/testdata/invalid-http-header-match.yml:
--------------------------------------------------------------------------------
1 | modules:
2 |   http_headers:
3 |     prober: http
4 |     timeout: 5s
5 |     http:
6 |       fail_if_header_not_matches:
7 |         - header: Access-Control-Allow-Origin
8 |           allow_missing: false
9 | 


--------------------------------------------------------------------------------
/config/testdata/invalid-http-request-compression-reject-all-encodings.yml:
--------------------------------------------------------------------------------
 1 | modules:
 2 |   http_headers:
 3 |     prober: http
 4 |     timeout: 5s
 5 |     http:
 6 |       # this configuration is invalid because it's requesting a
 7 |       # compressed encoding, but it's rejecting every possible encoding
 8 |       compression: gzip
 9 |       headers:
10 |         "Accept-Encoding": "*;q=0.0"
11 | 


--------------------------------------------------------------------------------
/config/testdata/invalid-icmp-ttl-overflow.yml:
--------------------------------------------------------------------------------
1 | modules:
2 |   icmp_test:
3 |     prober: icmp
4 |     icmp:
5 |       ttl: 256
6 | 


--------------------------------------------------------------------------------
/config/testdata/invalid-icmp-ttl.yml:
--------------------------------------------------------------------------------
1 | modules:
2 |   icmp_test:
3 |     prober: icmp
4 |     icmp:
5 |       ttl: -1
6 | 


--------------------------------------------------------------------------------
/config/testdata/invalid-tcp-query-response-regexp.yml:
--------------------------------------------------------------------------------
1 | modules:
2 |   tcp_test:
3 |     prober: tcp
4 |     timeout: 5s
5 |     tcp:
6 |       query_response:
7 |         - expect: ":["
8 |         - send: ". STARTTLS"
9 | 


--------------------------------------------------------------------------------
/example.yml:
--------------------------------------------------------------------------------
  1 | modules:
  2 |   http_2xx_example:
  3 |     prober: http
  4 |     timeout: 5s
  5 |     http:
  6 |       valid_http_versions: ["HTTP/1.1", "HTTP/2.0"]
  7 |       valid_status_codes: []  # Defaults to 2xx
  8 |       method: GET
  9 |       headers:
 10 |         Host: vhost.example.com
 11 |         Accept-Language: en-US
 12 |         Origin: example.com
 13 |       follow_redirects: true
 14 |       fail_if_ssl: false
 15 |       fail_if_not_ssl: false
 16 |       fail_if_body_matches_regexp:
 17 |         - "Could not connect to database"
 18 |       fail_if_body_not_matches_regexp:
 19 |         - "Download the latest version here"
 20 |       fail_if_header_matches: # Verifies that no cookies are set
 21 |         - header: Set-Cookie
 22 |           allow_missing: true
 23 |           regexp: '.*'
 24 |       fail_if_header_not_matches:
 25 |         - header: Access-Control-Allow-Origin
 26 |           regexp: '(\*|example\.com)'
 27 |       tls_config:
 28 |         insecure_skip_verify: false
 29 |       preferred_ip_protocol: "ip4" # defaults to "ip6"
 30 |       ip_protocol_fallback: false  # no fallback to "ip6"
 31 |   http_with_proxy:
 32 |     prober: http
 33 |     http:
 34 |       proxy_url: "http://127.0.0.1:3128"
 35 |       skip_resolve_phase_with_proxy: true
 36 |   http_with_proxy_and_headers:
 37 |     prober: http
 38 |     http:
 39 |       proxy_url: "http://127.0.0.1:3128"
 40 |       proxy_connect_header:
 41 |         Proxy-Authorization:
 42 |           - Bearer token
 43 |   http_post_2xx:
 44 |     prober: http
 45 |     timeout: 5s
 46 |     http:
 47 |       method: POST
 48 |       headers:
 49 |         Content-Type: application/json
 50 |       body: '{}'
 51 |   http_post_body_file:
 52 |     prober: http
 53 |     timeout: 5s
 54 |     http:
 55 |       method: POST
 56 |       body_file: "/files/body.txt"
 57 |   http_basic_auth_example:
 58 |     prober: http
 59 |     timeout: 5s
 60 |     http:
 61 |       method: POST
 62 |       headers:
 63 |         Host: "login.example.com"
 64 |       basic_auth:
 65 |         username: "username"
 66 |         password: "mysecret"
 67 |   http_json_cel_match:
 68 |     prober: http
 69 |     timeout: 5s
 70 |     http:
 71 |       method: GET
 72 |       fail_if_body_json_not_matches_cel: "body.foo == 'bar' && body.baz.startsWith('q')" # { "foo": "bar", "baz": "qux" }
 73 |   http_2xx_oauth_client_credentials:
 74 |     prober: http
 75 |     timeout: 5s
 76 |     http:
 77 |       valid_http_versions: ["HTTP/1.1", "HTTP/2"]
 78 |       follow_redirects: true
 79 |       preferred_ip_protocol: "ip4"
 80 |       valid_status_codes:
 81 |         - 200
 82 |         - 201
 83 |       oauth2:
 84 |         client_id: "client_id"
 85 |         client_secret: "client_secret"
 86 |         token_url: "https://api.example.com/token"
 87 |         endpoint_params:
 88 |           grant_type: "client_credentials"
 89 |   http_custom_ca_example:
 90 |     prober: http
 91 |     http:
 92 |       method: GET
 93 |       tls_config:
 94 |         ca_file: "/certs/my_cert.crt"
 95 |   http_gzip:
 96 |     prober: http
 97 |     http:
 98 |       method: GET
 99 |       compression: gzip
100 |   http_gzip_with_accept_encoding:
101 |     prober: http
102 |     http:
103 |       method: GET
104 |       compression: gzip
105 |       headers:
106 |         Accept-Encoding: gzip
107 |   tls_connect:
108 |     prober: tcp
109 |     timeout: 5s
110 |     tcp:
111 |       tls: true
112 |   tcp_connect_example:
113 |     prober: tcp
114 |     timeout: 5s
115 |   imap_starttls:
116 |     prober: tcp
117 |     timeout: 5s
118 |     tcp:
119 |       query_response:
120 |         - expect: "OK.*STARTTLS"
121 |         - send: ". STARTTLS"
122 |         - expect: "OK"
123 |         - starttls: true
124 |         - send: ". capability"
125 |         - expect: "CAPABILITY IMAP4rev1"
126 |   smtp_starttls:
127 |     prober: tcp
128 |     timeout: 5s
129 |     tcp:
130 |       query_response:
131 |         - expect: "^220 ([^ ]+) ESMTP (.+)
quot;
132 |         - send: "EHLO prober\r"
133 |         - expect: "^250-STARTTLS"
134 |         - send: "STARTTLS\r"
135 |         - expect: "^220"
136 |         - starttls: true
137 |         - send: "EHLO prober\r"
138 |         - expect: "^250-AUTH"
139 |         - send: "QUIT\r"
140 |   irc_banner_example:
141 |     prober: tcp
142 |     timeout: 5s
143 |     tcp:
144 |       query_response:
145 |         - send: "NICK prober"
146 |         - send: "USER prober prober prober :prober"
147 |         - expect: "PING :([^ ]+)"
148 |           send: "PONG ${1}"
149 |         - expect: "^:[^ ]+ 001"
150 |   rabbitmq:
151 |     prober: tcp
152 |     timeout: 30s
153 |     tcp:
154 |       query_response:
155 |         - send: "HELO\r"
156 |         - send: "\r"
157 |         - send: "\r"
158 |         - send: "\r"
159 |         - expect: "AMQP"
160 |       tls: true
161 |       tls_config:
162 |         insecure_skip_verify: false
163 |         ca_file: "/etc/blackbox_exporter/CA_cert.crt"
164 |   rabbitmq_insecure:
165 |     prober: tcp
166 |     timeout: 30s
167 |     tcp:
168 |       query_response:
169 |         - send: "HELO\r"
170 |         - send: "\r"
171 |         - send: "\r"
172 |         - send: "\r"
173 |         - expect: "AMQP"
174 |       tls: true
175 |       tls_config:
176 |         insecure_skip_verify: true
177 |         ca_file: "/etc/blackbox_exporter/CA_cert.crt"
178 |   icmp_example:
179 |     prober: icmp
180 |     timeout: 5s
181 |     icmp:
182 |       preferred_ip_protocol: "ip4"
183 |       source_ip_address: "127.0.0.1"
184 |   dns_udp_example:
185 |     prober: dns
186 |     timeout: 5s
187 |     dns:
188 |       query_name: "www.prometheus.io"
189 |       query_type: "A"
190 |       valid_rcodes:
191 |         - NOERROR
192 |       validate_answer_rrs:
193 |         fail_if_matches_regexp:
194 |           - ".*127.0.0.1"
195 |         fail_if_all_match_regexp:
196 |           - ".*127.0.0.1"
197 |         fail_if_not_matches_regexp:
198 |           - "www.prometheus.io.\t300\tIN\tA\t127.0.0.1"
199 |         fail_if_none_matches_regexp:
200 |           - "127.0.0.1"
201 |       validate_authority_rrs:
202 |         fail_if_matches_regexp:
203 |           - ".*127.0.0.1"
204 |       validate_additional_rrs:
205 |         fail_if_matches_regexp:
206 |           - ".*127.0.0.1"
207 |   dns_soa:
208 |     prober: dns
209 |     dns:
210 |       query_name: "prometheus.io"
211 |       query_type: "SOA"
212 |   dns_tcp_example:
213 |     prober: dns
214 |     dns:
215 |       transport_protocol: "tcp" # defaults to "udp"
216 |       preferred_ip_protocol: "ip4" # defaults to "ip6"
217 |       query_name: "www.prometheus.io"
218 | 


--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
 1 | module github.com/prometheus/blackbox_exporter
 2 | 
 3 | go 1.23.0
 4 | 
 5 | require (
 6 | 	github.com/alecthomas/kingpin/v2 v2.4.0
 7 | 	github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9
 8 | 	github.com/andybalholm/brotli v1.2.0
 9 | 	github.com/google/cel-go v0.25.0
10 | 	github.com/miekg/dns v1.1.66
11 | 	github.com/prometheus/client_golang v1.22.0
12 | 	github.com/prometheus/client_model v0.6.2
13 | 	github.com/prometheus/common v0.65.0
14 | 	github.com/prometheus/exporter-toolkit v0.14.0
15 | 	golang.org/x/net v0.41.0
16 | 	google.golang.org/grpc v1.73.0
17 | 	gopkg.in/yaml.v2 v2.4.0
18 | 	gopkg.in/yaml.v3 v3.0.1
19 | )
20 | 
21 | require (
22 | 	cel.dev/expr v0.23.1 // indirect
23 | 	github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
24 | 	github.com/beorn7/perks v1.0.1 // indirect
25 | 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
26 | 	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
27 | 	github.com/jpillora/backoff v1.0.0 // indirect
28 | 	github.com/mdlayher/socket v0.4.1 // indirect
29 | 	github.com/mdlayher/vsock v1.2.1 // indirect
30 | 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
31 | 	github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
32 | 	github.com/prometheus/procfs v0.15.1 // indirect
33 | 	github.com/stoewer/go-strcase v1.2.0 // indirect
34 | 	github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
35 | 	golang.org/x/crypto v0.39.0 // indirect
36 | 	golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
37 | 	golang.org/x/mod v0.25.0 // indirect
38 | 	golang.org/x/oauth2 v0.30.0 // indirect
39 | 	golang.org/x/sync v0.15.0 // indirect
40 | 	golang.org/x/sys v0.33.0 // indirect
41 | 	golang.org/x/text v0.26.0 // indirect
42 | 	golang.org/x/tools v0.33.0 // indirect
43 | 	google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
44 | 	google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
45 | 	google.golang.org/protobuf v1.36.6 // indirect
46 | )
47 | 


--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
  1 | cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg=
  2 | cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
  3 | github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
  4 | github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
  5 | github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 h1:ez/4by2iGztzR4L0zgAOR8lTQK9VlyBVVd7G4omaOQs=
  6 | github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
  7 | github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
  8 | github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
  9 | github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
 10 | github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
 11 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 12 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 13 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 14 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 15 | github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
 16 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 17 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 18 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 20 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
 21 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 22 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 23 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 24 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 25 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 26 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 27 | github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY=
 28 | github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI=
 29 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 30 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 31 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 32 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 33 | github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
 34 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
 35 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
 36 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
 37 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 38 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 39 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 40 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 41 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
 42 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
 43 | github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
 44 | github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
 45 | github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ=
 46 | github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE=
 47 | github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
 48 | github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
 49 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 50 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 51 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
 52 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 53 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 54 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 55 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
 56 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
 57 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
 58 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
 59 | github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
 60 | github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
 61 | github.com/prometheus/exporter-toolkit v0.14.0 h1:NMlswfibpcZZ+H0sZBiTjrA3/aBFHkNZqE+iCj5EmRg=
 62 | github.com/prometheus/exporter-toolkit v0.14.0/go.mod h1:Gu5LnVvt7Nr/oqTBUC23WILZepW0nffNo10XdhQcwWA=
 63 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 64 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 65 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
 66 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
 67 | github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
 68 | github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
 69 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 70 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 71 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 72 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 73 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 74 | github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
 75 | github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
 76 | github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
 77 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
 78 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 79 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
 80 | go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
 81 | go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
 82 | go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
 83 | go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
 84 | go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
 85 | go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
 86 | go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
 87 | go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
 88 | go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
 89 | go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
 90 | golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
 91 | golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
 92 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
 93 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
 94 | golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
 95 | golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
 96 | golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
 97 | golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
 98 | golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
 99 | golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
100 | golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
101 | golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
102 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
103 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
104 | golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
105 | golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
106 | golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
107 | golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
108 | google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
109 | google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
110 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
111 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
112 | google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
113 | google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
114 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
115 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
116 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
117 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
118 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
119 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
120 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
121 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
122 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
123 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
124 | 


--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
  1 | // Copyright 2016 The Prometheus Authors
  2 | // Licensed under the Apache License, Version 2.0 (the "License");
  3 | // you may not use this file except in compliance with the License.
  4 | // You may obtain a copy of the License at
  5 | //
  6 | // http://www.apache.org/licenses/LICENSE-2.0
  7 | //
  8 | // Unless required by applicable law or agreed to in writing, software
  9 | // distributed under the License is distributed on an "AS IS" BASIS,
 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 11 | // See the License for the specific language governing permissions and
 12 | // limitations under the License.
 13 | 
 14 | package main
 15 | 
 16 | import (
 17 | 	"errors"
 18 | 	"fmt"
 19 | 	"html"
 20 | 	"net"
 21 | 	"net/http"
 22 | 	_ "net/http/pprof"
 23 | 	"net/url"
 24 | 	"os"
 25 | 	"os/signal"
 26 | 	"path"
 27 | 	"strconv"
 28 | 	"strings"
 29 | 	"syscall"
 30 | 
 31 | 	"github.com/alecthomas/kingpin/v2"
 32 | 	"github.com/prometheus/client_golang/prometheus"
 33 | 	versioncollector "github.com/prometheus/client_golang/prometheus/collectors/version"
 34 | 	"github.com/prometheus/client_golang/prometheus/promauto"
 35 | 	"github.com/prometheus/client_golang/prometheus/promhttp"
 36 | 	"github.com/prometheus/common/promslog"
 37 | 	"github.com/prometheus/common/promslog/flag"
 38 | 	"github.com/prometheus/common/version"
 39 | 	"github.com/prometheus/exporter-toolkit/web"
 40 | 	webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag"
 41 | 	"gopkg.in/yaml.v3"
 42 | 
 43 | 	"github.com/prometheus/blackbox_exporter/config"
 44 | 	"github.com/prometheus/blackbox_exporter/prober"
 45 | )
 46 | 
 47 | var (
 48 | 	sc = config.NewSafeConfig(prometheus.DefaultRegisterer)
 49 | 
 50 | 	configFile     = kingpin.Flag("config.file", "Blackbox exporter configuration file.").Default("blackbox.yml").String()
 51 | 	timeoutOffset  = kingpin.Flag("timeout-offset", "Offset to subtract from timeout in seconds.").Default("0.5").Float64()
 52 | 	configCheck    = kingpin.Flag("config.check", "If true validate the config file and then exit.").Default().Bool()
 53 | 	logLevelProber = kingpin.Flag("log.prober", "Log level for probe request logs. One of: [debug, info, warn, error]. Defaults to debug. Please see the section `Controlling log level for probe logs` in the project README for more information.").Default("debug").String()
 54 | 	historyLimit   = kingpin.Flag("history.limit", "The maximum amount of items to keep in the history.").Default("100").Uint()
 55 | 	externalURL    = kingpin.Flag("web.external-url", "The URL under which Blackbox exporter is externally reachable (for example, if Blackbox exporter is served via a reverse proxy). Used for generating relative and absolute links back to Blackbox exporter itself. If the URL has a path portion, it will be used to prefix all HTTP endpoints served by Blackbox exporter. If omitted, relevant URL components will be derived automatically.").PlaceHolder("<url>").String()
 56 | 	routePrefix    = kingpin.Flag("web.route-prefix", "Prefix for the internal routes of web endpoints. Defaults to path of --web.external-url.").PlaceHolder("<path>").String()
 57 | 	toolkitFlags   = webflag.AddFlags(kingpin.CommandLine, ":9115")
 58 | 
 59 | 	moduleUnknownCounter = promauto.NewCounter(prometheus.CounterOpts{
 60 | 		Name: "blackbox_module_unknown_total",
 61 | 		Help: "Count of unknown modules requested by probes",
 62 | 	})
 63 | )
 64 | 
 65 | func init() {
 66 | 	prometheus.MustRegister(versioncollector.NewCollector("blackbox_exporter"))
 67 | }
 68 | 
 69 | func main() {
 70 | 	os.Exit(run())
 71 | }
 72 | 
 73 | func run() int {
 74 | 	kingpin.CommandLine.UsageWriter(os.Stdout)
 75 | 	promslogConfig := &promslog.Config{}
 76 | 	flag.AddFlags(kingpin.CommandLine, promslogConfig)
 77 | 	kingpin.Version(version.Print("blackbox_exporter"))
 78 | 	kingpin.HelpFlag.Short('h')
 79 | 	kingpin.Parse()
 80 | 	logger := promslog.New(promslogConfig)
 81 | 	rh := &prober.ResultHistory{MaxResults: *historyLimit}
 82 | 
 83 | 	probeLogLevel := promslog.NewLevel()
 84 | 	if err := probeLogLevel.Set(*logLevelProber); err != nil {
 85 | 		logger.Warn("Error setting log prober level, log prober level unchanged", "err", err, "current_level", probeLogLevel.String())
 86 | 	}
 87 | 
 88 | 	logger.Info("Starting blackbox_exporter", "version", version.Info())
 89 | 	logger.Info(version.BuildContext())
 90 | 
 91 | 	if err := sc.ReloadConfig(*configFile, logger); err != nil {
 92 | 		logger.Error("Error loading config", "err", err)
 93 | 		return 1
 94 | 	}
 95 | 
 96 | 	if *configCheck {
 97 | 		logger.Info("Config file is ok exiting...")
 98 | 		return 0
 99 | 	}
100 | 
101 | 	logger.Info("Loaded config file")
102 | 
103 | 	// Infer or set Blackbox exporter externalURL
104 | 	listenAddrs := toolkitFlags.WebListenAddresses
105 | 	if *externalURL == "" && *toolkitFlags.WebSystemdSocket {
106 | 		logger.Error("Cannot automatically infer external URL with systemd socket listener. Please provide --web.external-url")
107 | 		return 1
108 | 	} else if *externalURL == "" && len(*listenAddrs) > 1 {
109 | 		logger.Info("Inferring external URL from first provided listen address")
110 | 	}
111 | 	beURL, err := computeExternalURL(*externalURL, (*listenAddrs)[0])
112 | 	if err != nil {
113 | 		logger.Error("failed to determine external URL", "err", err)
114 | 		return 1
115 | 	}
116 | 	logger.Debug(beURL.String())
117 | 
118 | 	// Default -web.route-prefix to path of -web.external-url.
119 | 	if *routePrefix == "" {
120 | 		*routePrefix = beURL.Path
121 | 	}
122 | 
123 | 	// routePrefix must always be at least '/'.
124 | 	*routePrefix = "/" + strings.Trim(*routePrefix, "/")
125 | 	// routePrefix requires path to have trailing "/" in order
126 | 	// for browsers to interpret the path-relative path correctly, instead of stripping it.
127 | 	if *routePrefix != "/" {
128 | 		*routePrefix = *routePrefix + "/"
129 | 	}
130 | 	logger.Debug(*routePrefix)
131 | 
132 | 	hup := make(chan os.Signal, 1)
133 | 	reloadCh := make(chan chan error)
134 | 	signal.Notify(hup, syscall.SIGHUP)
135 | 	go func() {
136 | 		for {
137 | 			select {
138 | 			case <-hup:
139 | 				if err := sc.ReloadConfig(*configFile, logger); err != nil {
140 | 					logger.Error("Error reloading config", "err", err)
141 | 					continue
142 | 				}
143 | 				logger.Info("Reloaded config file")
144 | 			case rc := <-reloadCh:
145 | 				if err := sc.ReloadConfig(*configFile, logger); err != nil {
146 | 					logger.Error("Error reloading config", "err", err)
147 | 					rc <- err
148 | 				} else {
149 | 					logger.Info("Reloaded config file")
150 | 					rc <- nil
151 | 				}
152 | 			}
153 | 		}
154 | 	}()
155 | 
156 | 	// Match Prometheus behavior and redirect over externalURL for root path only
157 | 	// if routePrefix is different than "/"
158 | 	if *routePrefix != "/" {
159 | 		http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
160 | 			if r.URL.Path != "/" {
161 | 				http.NotFound(w, r)
162 | 				return
163 | 			}
164 | 			http.Redirect(w, r, beURL.String(), http.StatusFound)
165 | 		})
166 | 	}
167 | 
168 | 	http.HandleFunc(path.Join(*routePrefix, "/-/reload"),
169 | 		func(w http.ResponseWriter, r *http.Request) {
170 | 			if r.Method != "POST" {
171 | 				w.WriteHeader(http.StatusMethodNotAllowed)
172 | 				fmt.Fprintf(w, "This endpoint requires a POST request.\n")
173 | 				return
174 | 			}
175 | 
176 | 			rc := make(chan error)
177 | 			reloadCh <- rc
178 | 			if err := <-rc; err != nil {
179 | 				http.Error(w, fmt.Sprintf("failed to reload config: %s", err), http.StatusInternalServerError)
180 | 			}
181 | 		})
182 | 	http.Handle(path.Join(*routePrefix, "/metrics"), promhttp.Handler())
183 | 	http.HandleFunc(path.Join(*routePrefix, "/-/healthy"), func(w http.ResponseWriter, r *http.Request) {
184 | 		w.WriteHeader(http.StatusOK)
185 | 		w.Write([]byte("Healthy"))
186 | 	})
187 | 	http.HandleFunc(path.Join(*routePrefix, "/probe"), func(w http.ResponseWriter, r *http.Request) {
188 | 		sc.Lock()
189 | 		conf := sc.C
190 | 		sc.Unlock()
191 | 		prober.Handler(w, r, conf, logger, rh, *timeoutOffset, nil, moduleUnknownCounter, promslogConfig.Level, probeLogLevel)
192 | 	})
193 | 	http.HandleFunc(*routePrefix, func(w http.ResponseWriter, r *http.Request) {
194 | 		w.Header().Set("Content-Type", "text/html")
195 | 		w.Write([]byte(`<html>
196 |     <head><title>Blackbox Exporter</title></head>
197 |     <body>
198 |     <h1>Blackbox Exporter</h1>
199 |     <p><a href="probe?target=prometheus.io&module=http_2xx">Probe prometheus.io for http_2xx</a></p>
200 |     <p><a href="probe?target=prometheus.io&module=http_2xx&debug=true">Debug probe prometheus.io for http_2xx</a></p>
201 |     <p><a href="metrics">Metrics</a></p>
202 |     <p><a href="config">Configuration</a></p>
203 |     <h2>Recent Probes</h2>
204 |     <table border='1'><tr><th>Module</th><th>Target</th><th>Result</th><th>Debug</th>`))
205 | 
206 | 		results := rh.List()
207 | 
208 | 		for i := len(results) - 1; i >= 0; i-- {
209 | 			r := results[i]
210 | 			success := "Success"
211 | 			if !r.Success {
212 | 				success = "<strong>Failure</strong>"
213 | 			}
214 | 			fmt.Fprintf(w, "<tr><td>%s</td><td>%s</td><td>%s</td><td><a href='logs?id=%d'>Logs</a></td></td>",
215 | 				html.EscapeString(r.ModuleName), html.EscapeString(r.Target), success, r.Id)
216 | 		}
217 | 
218 | 		w.Write([]byte(`</table></body>
219 |     </html>`))
220 | 	})
221 | 
222 | 	http.HandleFunc(path.Join(*routePrefix, "/logs"), func(w http.ResponseWriter, r *http.Request) {
223 | 		id, err := strconv.ParseInt(r.URL.Query().Get("id"), 10, 64)
224 | 		if err != nil {
225 | 			id = -1
226 | 		}
227 | 		target := r.URL.Query().Get("target")
228 | 		if err == nil && target != "" {
229 | 			http.Error(w, "Probe id and target can't be defined at the same time", http.StatusBadRequest)
230 | 			return
231 | 		}
232 | 		if id == -1 && target == "" {
233 | 			http.Error(w, "Probe id or target must be defined as http query parameters", http.StatusBadRequest)
234 | 			return
235 | 		}
236 | 		result := new(prober.Result)
237 | 		if target != "" {
238 | 			result = rh.GetByTarget(target)
239 | 			if result == nil {
240 | 				http.Error(w, "Probe target not found", http.StatusNotFound)
241 | 				return
242 | 			}
243 | 		} else {
244 | 			result = rh.GetById(id)
245 | 			if result == nil {
246 | 				http.Error(w, "Probe id not found", http.StatusNotFound)
247 | 				return
248 | 			}
249 | 		}
250 | 
251 | 		w.Header().Set("Content-Type", "text/plain")
252 | 		w.Write([]byte(result.DebugOutput))
253 | 	})
254 | 
255 | 	http.HandleFunc(path.Join(*routePrefix, "/config"), func(w http.ResponseWriter, r *http.Request) {
256 | 		sc.RLock()
257 | 		c, err := yaml.Marshal(sc.C)
258 | 		sc.RUnlock()
259 | 		if err != nil {
260 | 			logger.Warn("Error marshalling configuration", "err", err)
261 | 			http.Error(w, err.Error(), http.StatusInternalServerError)
262 | 			return
263 | 		}
264 | 		w.Header().Set("Content-Type", "text/plain")
265 | 		w.Write(c)
266 | 	})
267 | 
268 | 	srv := &http.Server{}
269 | 	srvc := make(chan struct{})
270 | 	term := make(chan os.Signal, 1)
271 | 	signal.Notify(term, os.Interrupt, syscall.SIGTERM)
272 | 
273 | 	go func() {
274 | 		if err := web.ListenAndServe(srv, toolkitFlags, logger); err != nil {
275 | 			logger.Error("Error starting HTTP server", "err", err)
276 | 			close(srvc)
277 | 		}
278 | 	}()
279 | 
280 | 	for {
281 | 		select {
282 | 		case <-term:
283 | 			logger.Info("Received SIGTERM, exiting gracefully...")
284 | 			return 0
285 | 		case <-srvc:
286 | 			return 1
287 | 		}
288 | 	}
289 | 
290 | }
291 | 
292 | func startsOrEndsWithQuote(s string) bool {
293 | 	return strings.HasPrefix(s, "\"") || strings.HasPrefix(s, "'") ||
294 | 		strings.HasSuffix(s, "\"") || strings.HasSuffix(s, "'")
295 | }
296 | 
297 | // computeExternalURL computes a sanitized external URL from a raw input. It infers unset
298 | // URL parts from the OS and the given listen address.
299 | func computeExternalURL(u, listenAddr string) (*url.URL, error) {
300 | 	if u == "" {
301 | 		hostname, err := os.Hostname()
302 | 		if err != nil {
303 | 			return nil, err
304 | 		}
305 | 		_, port, err := net.SplitHostPort(listenAddr)
306 | 		if err != nil {
307 | 			return nil, err
308 | 		}
309 | 		u = fmt.Sprintf("http://%s:%s/", hostname, port)
310 | 	}
311 | 
312 | 	if startsOrEndsWithQuote(u) {
313 | 		return nil, errors.New("URL must not begin or end with quotes")
314 | 	}
315 | 
316 | 	eu, err := url.Parse(u)
317 | 	if err != nil {
318 | 		return nil, err
319 | 	}
320 | 
321 | 	ppref := strings.TrimRight(eu.Path, "/")
322 | 	if ppref != "" && !strings.HasPrefix(ppref, "/") {
323 | 		ppref = "/" + ppref
324 | 	}
325 | 	eu.Path = ppref
326 | 
327 | 	return eu, nil
328 | }
329 | 


--------------------------------------------------------------------------------
/main_test.go:
--------------------------------------------------------------------------------
 1 | // Copyright 2016 The Prometheus Authors
 2 | // Licensed under the Apache License, Version 2.0 (the "License");
 3 | // you may not use this file except in compliance with the License.
 4 | // You may obtain a copy of the License at
 5 | //
 6 | // http://www.apache.org/licenses/LICENSE-2.0
 7 | //
 8 | // Unless required by applicable law or agreed to in writing, software
 9 | // distributed under the License is distributed on an "AS IS" BASIS,
10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | // See the License for the specific language governing permissions and
12 | // limitations under the License.
13 | 
14 | package main
15 | 
16 | import "testing"
17 | 
18 | func TestComputeExternalURL(t *testing.T) {
19 | 	tests := []struct {
20 | 		input string
21 | 		valid bool
22 | 	}{
23 | 		{
24 | 			input: "",
25 | 			valid: true,
26 | 		},
27 | 		{
28 | 			input: "http://proxy.com/prometheus",
29 | 			valid: true,
30 | 		},
31 | 		{
32 | 			input: "'https://url/prometheus'",
33 | 			valid: false,
34 | 		},
35 | 		{
36 | 			input: "'relative/path/with/quotes'",
37 | 			valid: false,
38 | 		},
39 | 		{
40 | 			input: "http://alertmanager.company.com",
41 | 			valid: true,
42 | 		},
43 | 		{
44 | 			input: "https://double--dash.de",
45 | 			valid: true,
46 | 		},
47 | 		{
48 | 			input: "'http://starts/with/quote",
49 | 			valid: false,
50 | 		},
51 | 		{
52 | 			input: "ends/with/quote\"",
53 | 			valid: false,
54 | 		},
55 | 	}
56 | 
57 | 	for _, test := range tests {
58 | 		_, err := computeExternalURL(test.input, "0.0.0.0:9090")
59 | 		if test.valid {
60 | 			if err != nil {
61 | 				t.Errorf("unexpected error %v", err)
62 | 			}
63 | 		} else {
64 | 			if err == nil {
65 | 				t.Errorf("expected error computing %s got none", test.input)
66 | 			}
67 | 		}
68 | 	}
69 | }
70 | 


--------------------------------------------------------------------------------
/prober/dns.go:
--------------------------------------------------------------------------------
  1 | // Copyright 2016 The Prometheus Authors
  2 | // Licensed under the Apache License, Version 2.0 (the "License");
  3 | // you may not use this file except in compliance with the License.
  4 | // You may obtain a copy of the License at
  5 | //
  6 | // http://www.apache.org/licenses/LICENSE-2.0
  7 | //
  8 | // Unless required by applicable law or agreed to in writing, software
  9 | // distributed under the License is distributed on an "AS IS" BASIS,
 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 11 | // See the License for the specific language governing permissions and
 12 | // limitations under the License.
 13 | 
 14 | package prober
 15 | 
 16 | import (
 17 | 	"context"
 18 | 	"log/slog"
 19 | 	"net"
 20 | 	"regexp"
 21 | 	"time"
 22 | 
 23 | 	"github.com/miekg/dns"
 24 | 	"github.com/prometheus/client_golang/prometheus"
 25 | 	pconfig "github.com/prometheus/common/config"
 26 | 
 27 | 	"github.com/prometheus/blackbox_exporter/config"
 28 | )
 29 | 
 30 | // validRRs checks a slice of RRs received from the server against a DNSRRValidator.
 31 | func validRRs(rrs *[]dns.RR, v *config.DNSRRValidator, logger *slog.Logger) bool {
 32 | 	var anyMatch = false
 33 | 	var allMatch = true
 34 | 	// Fail the probe if there are no RRs of a given type, but a regexp match is required
 35 | 	// (i.e. FailIfNotMatchesRegexp or FailIfNoneMatchesRegexp is set).
 36 | 	if len(*rrs) == 0 && len(v.FailIfNotMatchesRegexp) > 0 {
 37 | 		logger.Error("fail_if_not_matches_regexp specified but no RRs returned")
 38 | 		return false
 39 | 	}
 40 | 	if len(*rrs) == 0 && len(v.FailIfNoneMatchesRegexp) > 0 {
 41 | 		logger.Error("fail_if_none_matches_regexp specified but no RRs returned")
 42 | 		return false
 43 | 	}
 44 | 	for _, rr := range *rrs {
 45 | 		logger.Info("Validating RR", "rr", rr)
 46 | 		for _, re := range v.FailIfMatchesRegexp {
 47 | 			match, err := regexp.MatchString(re, rr.String())
 48 | 			if err != nil {
 49 | 				logger.Error("Error matching regexp", "regexp", re, "err", err)
 50 | 				return false
 51 | 			}
 52 | 			if match {
 53 | 				logger.Error("At least one RR matched regexp", "regexp", re, "rr", rr)
 54 | 				return false
 55 | 			}
 56 | 		}
 57 | 		for _, re := range v.FailIfAllMatchRegexp {
 58 | 			match, err := regexp.MatchString(re, rr.String())
 59 | 			if err != nil {
 60 | 				logger.Error("Error matching regexp", "regexp", re, "err", err)
 61 | 				return false
 62 | 			}
 63 | 			if !match {
 64 | 				allMatch = false
 65 | 			}
 66 | 		}
 67 | 		for _, re := range v.FailIfNotMatchesRegexp {
 68 | 			match, err := regexp.MatchString(re, rr.String())
 69 | 			if err != nil {
 70 | 				logger.Error("Error matching regexp", "regexp", re, "err", err)
 71 | 				return false
 72 | 			}
 73 | 			if !match {
 74 | 				logger.Error("At least one RR did not match regexp", "regexp", re, "rr", rr)
 75 | 				return false
 76 | 			}
 77 | 		}
 78 | 		for _, re := range v.FailIfNoneMatchesRegexp {
 79 | 			match, err := regexp.MatchString(re, rr.String())
 80 | 			if err != nil {
 81 | 				logger.Error("Error matching regexp", "regexp", re, "err", err)
 82 | 				return false
 83 | 			}
 84 | 			if match {
 85 | 				anyMatch = true
 86 | 			}
 87 | 		}
 88 | 	}
 89 | 	if len(v.FailIfAllMatchRegexp) > 0 && !allMatch {
 90 | 		logger.Error("Not all RRs matched regexp")
 91 | 		return false
 92 | 	}
 93 | 	if len(v.FailIfNoneMatchesRegexp) > 0 && !anyMatch {
 94 | 		logger.Error("None of the RRs did matched any regexp")
 95 | 		return false
 96 | 	}
 97 | 	return true
 98 | }
 99 | 
100 | // validRcode checks rcode in the response against a list of valid rcodes.
101 | func validRcode(rcode int, valid []string, logger *slog.Logger) bool {
102 | 	var validRcodes []int
103 | 	// If no list of valid rcodes is specified, only NOERROR is considered valid.
104 | 	if valid == nil {
105 | 		validRcodes = append(validRcodes, dns.StringToRcode["NOERROR"])
106 | 	} else {
107 | 		for _, rcode := range valid {
108 | 			rc, ok := dns.StringToRcode[rcode]
109 | 			if !ok {
110 | 				logger.Error("Invalid rcode", "rcode", rcode, "known_rcode", dns.RcodeToString)
111 | 				return false
112 | 			}
113 | 			validRcodes = append(validRcodes, rc)
114 | 		}
115 | 	}
116 | 	for _, rc := range validRcodes {
117 | 		if rcode == rc {
118 | 			logger.Info("Rcode is valid", "rcode", rcode, "string_rcode", dns.RcodeToString[rcode])
119 | 			return true
120 | 		}
121 | 	}
122 | 	logger.Error("Rcode is not one of the valid rcodes", "rcode", rcode, "string_rcode", dns.RcodeToString[rcode], "valid_rcodes", validRcodes)
123 | 	return false
124 | }
125 | 
126 | func ProbeDNS(ctx context.Context, target string, module config.Module, registry *prometheus.Registry, logger *slog.Logger) bool {
127 | 	var dialProtocol string
128 | 	probeDNSDurationGaugeVec := prometheus.NewGaugeVec(prometheus.GaugeOpts{
129 | 		Name: "probe_dns_duration_seconds",
130 | 		Help: "Duration of DNS request by phase",
131 | 	}, []string{"phase"})
132 | 	probeDNSAnswerRRSGauge := prometheus.NewGauge(prometheus.GaugeOpts{
133 | 		Name: "probe_dns_answer_rrs",
134 | 		Help: "Returns number of entries in the answer resource record list",
135 | 	})
136 | 	probeDNSAuthorityRRSGauge := prometheus.NewGauge(prometheus.GaugeOpts{
137 | 		Name: "probe_dns_authority_rrs",
138 | 		Help: "Returns number of entries in the authority resource record list",
139 | 	})
140 | 	probeDNSAdditionalRRSGauge := prometheus.NewGauge(prometheus.GaugeOpts{
141 | 		Name: "probe_dns_additional_rrs",
142 | 		Help: "Returns number of entries in the additional resource record list",
143 | 	})
144 | 	probeDNSQuerySucceeded := prometheus.NewGauge(prometheus.GaugeOpts{
145 | 		Name: "probe_dns_query_succeeded",
146 | 		Help: "Displays whether or not the query was executed successfully",
147 | 	})
148 | 
149 | 	for _, lv := range []string{"resolve", "connect", "request"} {
150 | 		probeDNSDurationGaugeVec.WithLabelValues(lv)
151 | 	}
152 | 
153 | 	registry.MustRegister(probeDNSDurationGaugeVec)
154 | 	registry.MustRegister(probeDNSAnswerRRSGauge)
155 | 	registry.MustRegister(probeDNSAuthorityRRSGauge)
156 | 	registry.MustRegister(probeDNSAdditionalRRSGauge)
157 | 	registry.MustRegister(probeDNSQuerySucceeded)
158 | 
159 | 	qc := uint16(dns.ClassINET)
160 | 	if module.DNS.QueryClass != "" {
161 | 		var ok bool
162 | 		qc, ok = dns.StringToClass[module.DNS.QueryClass]
163 | 		if !ok {
164 | 			logger.Error("Invalid query class", "Class seen", module.DNS.QueryClass, "Existing classes", dns.ClassToString)
165 | 			return false
166 | 		}
167 | 	}
168 | 
169 | 	qt := dns.TypeANY
170 | 	if module.DNS.QueryType != "" {
171 | 		var ok bool
172 | 		qt, ok = dns.StringToType[module.DNS.QueryType]
173 | 		if !ok {
174 | 			logger.Error("Invalid query type", "Type seen", module.DNS.QueryType, "Existing types", dns.TypeToString)
175 | 			return false
176 | 		}
177 | 	}
178 | 	var probeDNSSOAGauge prometheus.Gauge
179 | 
180 | 	var ip *net.IPAddr
181 | 	if module.DNS.TransportProtocol == "" {
182 | 		module.DNS.TransportProtocol = "udp"
183 | 	}
184 | 	if module.DNS.TransportProtocol != "udp" && module.DNS.TransportProtocol != "tcp" {
185 | 		logger.Error("Configuration error: Expected transport protocol udp or tcp", "protocol", module.DNS.TransportProtocol)
186 | 		return false
187 | 	}
188 | 
189 | 	targetAddr, port, err := net.SplitHostPort(target)
190 | 	if err != nil {
191 | 		// Target only contains host so fallback to default port and set targetAddr as target.
192 | 		if module.DNS.DNSOverTLS {
193 | 			port = "853"
194 | 		} else {
195 | 			port = "53"
196 | 		}
197 | 		targetAddr = target
198 | 	}
199 | 	ip, lookupTime, err := chooseProtocol(ctx, module.DNS.IPProtocol, module.DNS.IPProtocolFallback, targetAddr, registry, logger)
200 | 	if err != nil {
201 | 		logger.Error("Error resolving address", "err", err)
202 | 		return false
203 | 	}
204 | 	probeDNSDurationGaugeVec.WithLabelValues("resolve").Add(lookupTime)
205 | 	targetIP := net.JoinHostPort(ip.String(), port)
206 | 
207 | 	if ip.IP.To4() == nil {
208 | 		dialProtocol = module.DNS.TransportProtocol + "6"
209 | 	} else {
210 | 		dialProtocol = module.DNS.TransportProtocol + "4"
211 | 	}
212 | 
213 | 	if module.DNS.DNSOverTLS {
214 | 		if module.DNS.TransportProtocol == "tcp" {
215 | 			dialProtocol += "-tls"
216 | 		} else {
217 | 			logger.Error("Configuration error: Expected transport protocol tcp for DoT", "protocol", module.DNS.TransportProtocol)
218 | 			return false
219 | 		}
220 | 	}
221 | 
222 | 	client := new(dns.Client)
223 | 	client.Net = dialProtocol
224 | 
225 | 	if module.DNS.DNSOverTLS {
226 | 		tlsConfig, err := pconfig.NewTLSConfig(&module.DNS.TLSConfig)
227 | 		if err != nil {
228 | 			logger.Error("Failed to create TLS configuration", "err", err)
229 | 			return false
230 | 		}
231 | 		if tlsConfig.ServerName == "" {
232 | 			// Use target-hostname as default for TLS-servername.
233 | 			tlsConfig.ServerName = targetAddr
234 | 		}
235 | 
236 | 		client.TLSConfig = tlsConfig
237 | 	}
238 | 
239 | 	// Use configured SourceIPAddress.
240 | 	if len(module.DNS.SourceIPAddress) > 0 {
241 | 		srcIP := net.ParseIP(module.DNS.SourceIPAddress)
242 | 		if srcIP == nil {
243 | 			logger.Error("Error parsing source ip address", "srcIP", module.DNS.SourceIPAddress)
244 | 			return false
245 | 		}
246 | 		logger.Info("Using local address", "srcIP", srcIP)
247 | 		client.Dialer = &net.Dialer{}
248 | 		if module.DNS.TransportProtocol == "tcp" {
249 | 			client.Dialer.LocalAddr = &net.TCPAddr{IP: srcIP}
250 | 		} else {
251 | 			client.Dialer.LocalAddr = &net.UDPAddr{IP: srcIP}
252 | 		}
253 | 	}
254 | 
255 | 	msg := new(dns.Msg)
256 | 	msg.Id = dns.Id()
257 | 	msg.RecursionDesired = module.DNS.Recursion
258 | 	msg.Question = make([]dns.Question, 1)
259 | 	msg.Question[0] = dns.Question{dns.Fqdn(module.DNS.QueryName), qt, qc}
260 | 
261 | 	logger.Info("Making DNS query", "target", targetIP, "dial_protocol", dialProtocol, "query", module.DNS.QueryName, "type", qt, "class", qc)
262 | 	timeoutDeadline, _ := ctx.Deadline()
263 | 	client.Timeout = time.Until(timeoutDeadline)
264 | 	requestStart := time.Now()
265 | 	response, rtt, err := client.Exchange(msg, targetIP)
266 | 	// The rtt value returned from client.Exchange includes only the time to
267 | 	// exchange messages with the server _after_ the connection is created.
268 | 	// We compute the connection time as the total time for the operation
269 | 	// minus the time for the actual request rtt.
270 | 	probeDNSDurationGaugeVec.WithLabelValues("connect").Set((time.Since(requestStart) - rtt).Seconds())
271 | 	probeDNSDurationGaugeVec.WithLabelValues("request").Set(rtt.Seconds())
272 | 	if err != nil {
273 | 		logger.Error("Error while sending a DNS query", "err", err)
274 | 		return false
275 | 	}
276 | 	logger.Info("Got response", "response", response)
277 | 
278 | 	probeDNSAnswerRRSGauge.Set(float64(len(response.Answer)))
279 | 	probeDNSAuthorityRRSGauge.Set(float64(len(response.Ns)))
280 | 	probeDNSAdditionalRRSGauge.Set(float64(len(response.Extra)))
281 | 	probeDNSQuerySucceeded.Set(1)
282 | 
283 | 	if qt == dns.TypeSOA {
284 | 		probeDNSSOAGauge = prometheus.NewGauge(prometheus.GaugeOpts{
285 | 			Name: "probe_dns_serial",
286 | 			Help: "Returns the serial number of the zone",
287 | 		})
288 | 		registry.MustRegister(probeDNSSOAGauge)
289 | 
290 | 		for _, a := range response.Answer {
291 | 			if soa, ok := a.(*dns.SOA); ok {
292 | 				probeDNSSOAGauge.Set(float64(soa.Serial))
293 | 			}
294 | 		}
295 | 	}
296 | 
297 | 	if !validRcode(response.Rcode, module.DNS.ValidRcodes, logger) {
298 | 		return false
299 | 	}
300 | 	logger.Info("Validating Answer RRs")
301 | 	if !validRRs(&response.Answer, &module.DNS.ValidateAnswer, logger) {
302 | 		logger.Error("Answer RRs validation failed")
303 | 		return false
304 | 	}
305 | 	logger.Info("Validating Authority RRs")
306 | 	if !validRRs(&response.Ns, &module.DNS.ValidateAuthority, logger) {
307 | 		logger.Error("Authority RRs validation failed")
308 | 		return false
309 | 	}
310 | 	logger.Info("Validating Additional RRs")
311 | 	if !validRRs(&response.Extra, &module.DNS.ValidateAdditional, logger) {
312 | 		logger.Error("Additional RRs validation failed")
313 | 		return false
314 | 	}
315 | 	return true
316 | }
317 | 


--------------------------------------------------------------------------------
/prober/grpc.go:
--------------------------------------------------------------------------------
  1 | // Copyright 2021 The Prometheus Authors
  2 | // Licensed under the Apache License, Version 2.0 (the "License");
  3 | // you may not use this file except in compliance with the License.
  4 | // You may obtain a copy of the License at
  5 | //
  6 | // http://www.apache.org/licenses/LICENSE-2.0
  7 | //
  8 | // Unless required by applicable law or agreed to in writing, software
  9 | // distributed under the License is distributed on an "AS IS" BASIS,
 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 11 | // See the License for the specific language governing permissions and
 12 | // limitations under the License.
 13 | 
 14 | package prober
 15 | 
 16 | import (
 17 | 	"context"
 18 | 	"log/slog"
 19 | 	"net"
 20 | 	"net/url"
 21 | 	"strings"
 22 | 	"time"
 23 | 
 24 | 	"github.com/prometheus/blackbox_exporter/config"
 25 | 	"github.com/prometheus/client_golang/prometheus"
 26 | 	pconfig "github.com/prometheus/common/config"
 27 | 	"google.golang.org/grpc"
 28 | 	"google.golang.org/grpc/codes"
 29 | 	"google.golang.org/grpc/credentials"
 30 | 	"google.golang.org/grpc/credentials/insecure"
 31 | 	"google.golang.org/grpc/health/grpc_health_v1"
 32 | 	"google.golang.org/grpc/peer"
 33 | 	"google.golang.org/grpc/status"
 34 | )
 35 | 
 36 | type GRPCHealthCheck interface {
 37 | 	Check(c context.Context, service string) (bool, codes.Code, *peer.Peer, string, error)
 38 | }
 39 | 
 40 | type gRPCHealthCheckClient struct {
 41 | 	client grpc_health_v1.HealthClient
 42 | 	conn   *grpc.ClientConn
 43 | }
 44 | 
 45 | func NewGrpcHealthCheckClient(conn *grpc.ClientConn) GRPCHealthCheck {
 46 | 	client := new(gRPCHealthCheckClient)
 47 | 	client.client = grpc_health_v1.NewHealthClient(conn)
 48 | 	client.conn = conn
 49 | 	return client
 50 | }
 51 | 
 52 | func (c *gRPCHealthCheckClient) Close() error {
 53 | 	return c.conn.Close()
 54 | }
 55 | 
 56 | func (c *gRPCHealthCheckClient) Check(ctx context.Context, service string) (bool, codes.Code, *peer.Peer, string, error) {
 57 | 	var res *grpc_health_v1.HealthCheckResponse
 58 | 	var err error
 59 | 	req := grpc_health_v1.HealthCheckRequest{
 60 | 		Service: service,
 61 | 	}
 62 | 
 63 | 	serverPeer := new(peer.Peer)
 64 | 	res, err = c.client.Check(ctx, &req, grpc.Peer(serverPeer))
 65 | 	if err == nil {
 66 | 		if res.GetStatus() == grpc_health_v1.HealthCheckResponse_SERVING {
 67 | 			return true, codes.OK, serverPeer, res.Status.String(), nil
 68 | 		}
 69 | 		return false, codes.OK, serverPeer, res.Status.String(), nil
 70 | 	}
 71 | 
 72 | 	returnStatus, _ := status.FromError(err)
 73 | 
 74 | 	return false, returnStatus.Code(), nil, "", err
 75 | }
 76 | 
 77 | func ProbeGRPC(ctx context.Context, target string, module config.Module, registry *prometheus.Registry, logger *slog.Logger) (success bool) {
 78 | 
 79 | 	var (
 80 | 		durationGaugeVec = prometheus.NewGaugeVec(prometheus.GaugeOpts{
 81 | 			Name: "probe_grpc_duration_seconds",
 82 | 			Help: "Duration of gRPC request by phase",
 83 | 		}, []string{"phase"})
 84 | 
 85 | 		isSSLGauge = prometheus.NewGauge(prometheus.GaugeOpts{
 86 | 			Name: "probe_grpc_ssl",
 87 | 			Help: "Indicates if SSL was used for the connection",
 88 | 		})
 89 | 
 90 | 		statusCodeGauge = prometheus.NewGauge(prometheus.GaugeOpts{
 91 | 			Name: "probe_grpc_status_code",
 92 | 			Help: "Response gRPC status code",
 93 | 		})
 94 | 
 95 | 		healthCheckResponseGaugeVec = prometheus.NewGaugeVec(prometheus.GaugeOpts{
 96 | 			Name: "probe_grpc_healthcheck_response",
 97 | 			Help: "Response HealthCheck response",
 98 | 		}, []string{"serving_status"})
 99 | 
100 | 		probeSSLEarliestCertExpiryGauge = prometheus.NewGauge(sslEarliestCertExpiryGaugeOpts)
101 | 
102 | 		probeTLSVersion = prometheus.NewGaugeVec(
103 | 			probeTLSInfoGaugeOpts,
104 | 			[]string{"version"},
105 | 		)
106 | 
107 | 		probeSSLLastInformation = prometheus.NewGaugeVec(
108 | 			prometheus.GaugeOpts{
109 | 				Name: "probe_ssl_last_chain_info",
110 | 				Help: "Contains SSL leaf certificate information",
111 | 			},
112 | 			[]string{"fingerprint_sha256", "subject", "issuer", "subjectalternative", "serialnumber"},
113 | 		)
114 | 	)
115 | 
116 | 	for _, lv := range []string{"resolve"} {
117 | 		durationGaugeVec.WithLabelValues(lv)
118 | 	}
119 | 
120 | 	registry.MustRegister(durationGaugeVec)
121 | 	registry.MustRegister(isSSLGauge)
122 | 	registry.MustRegister(statusCodeGauge)
123 | 	registry.MustRegister(healthCheckResponseGaugeVec)
124 | 
125 | 	if !strings.HasPrefix(target, "http://") && !strings.HasPrefix(target, "https://") {
126 | 		target = "http://" + target
127 | 	}
128 | 
129 | 	targetURL, err := url.Parse(target)
130 | 	if err != nil {
131 | 		logger.Error("Could not parse target URL", "err", err)
132 | 		return false
133 | 	}
134 | 
135 | 	targetHost, targetPort, err := net.SplitHostPort(targetURL.Host)
136 | 	// If split fails, assuming it's a hostname without port part.
137 | 	if err != nil {
138 | 		targetHost = targetURL.Host
139 | 	}
140 | 
141 | 	tlsConfig, err := pconfig.NewTLSConfig(&module.GRPC.TLSConfig)
142 | 	if err != nil {
143 | 		logger.Error("Error creating TLS configuration", "err", err)
144 | 		return false
145 | 	}
146 | 
147 | 	ip, lookupTime, err := chooseProtocol(ctx, module.GRPC.PreferredIPProtocol, module.GRPC.IPProtocolFallback, targetHost, registry, logger)
148 | 	if err != nil {
149 | 		logger.Error("Error resolving address", "err", err)
150 | 		return false
151 | 	}
152 | 	durationGaugeVec.WithLabelValues("resolve").Add(lookupTime)
153 | 	checkStart := time.Now()
154 | 	if len(tlsConfig.ServerName) == 0 {
155 | 		// If there is no `server_name` in tls_config, use
156 | 		// the hostname of the target.
157 | 		tlsConfig.ServerName = targetHost
158 | 	}
159 | 
160 | 	if targetPort == "" {
161 | 		targetURL.Host = "[" + ip.String() + "]"
162 | 	} else {
163 | 		targetURL.Host = net.JoinHostPort(ip.String(), targetPort)
164 | 	}
165 | 
166 | 	var opts []grpc.DialOption
167 | 	target = targetHost + ":" + targetPort
168 | 	if !module.GRPC.TLS {
169 | 		logger.Debug("Dialing GRPC without TLS")
170 | 		opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
171 | 		if len(targetPort) == 0 {
172 | 			target = targetHost + ":80"
173 | 		}
174 | 	} else {
175 | 		creds := credentials.NewTLS(tlsConfig)
176 | 		opts = append(opts, grpc.WithTransportCredentials(creds))
177 | 		if len(targetPort) == 0 {
178 | 			target = targetHost + ":443"
179 | 		}
180 | 	}
181 | 
182 | 	conn, err := grpc.NewClient(target, opts...)
183 | 
184 | 	if err != nil {
185 | 		logger.Error("did not connect", "err", err)
186 | 	}
187 | 
188 | 	client := NewGrpcHealthCheckClient(conn)
189 | 	defer conn.Close()
190 | 	ok, statusCode, serverPeer, servingStatus, err := client.Check(context.Background(), module.GRPC.Service)
191 | 	durationGaugeVec.WithLabelValues("check").Add(time.Since(checkStart).Seconds())
192 | 
193 | 	for servingStatusName := range grpc_health_v1.HealthCheckResponse_ServingStatus_value {
194 | 		healthCheckResponseGaugeVec.WithLabelValues(servingStatusName).Set(float64(0))
195 | 	}
196 | 	if servingStatus != "" {
197 | 		healthCheckResponseGaugeVec.WithLabelValues(servingStatus).Set(float64(1))
198 | 	}
199 | 
200 | 	if serverPeer != nil {
201 | 		tlsInfo, tlsOk := serverPeer.AuthInfo.(credentials.TLSInfo)
202 | 		if tlsOk {
203 | 			registry.MustRegister(probeSSLEarliestCertExpiryGauge, probeTLSVersion, probeSSLLastInformation)
204 | 			isSSLGauge.Set(float64(1))
205 | 			probeSSLEarliestCertExpiryGauge.Set(float64(getEarliestCertExpiry(&tlsInfo.State).Unix()))
206 | 			probeTLSVersion.WithLabelValues(getTLSVersion(&tlsInfo.State)).Set(1)
207 | 			probeSSLLastInformation.WithLabelValues(getFingerprint(&tlsInfo.State), getSubject(&tlsInfo.State), getIssuer(&tlsInfo.State), getDNSNames(&tlsInfo.State), getSerialNumber(&tlsInfo.State)).Set(1)
208 | 		} else {
209 | 			isSSLGauge.Set(float64(0))
210 | 		}
211 | 	}
212 | 	statusCodeGauge.Set(float64(statusCode))
213 | 
214 | 	if !ok || err != nil {
215 | 		logger.Error("can't connect grpc server:", "err", err)
216 | 		success = false
217 | 	} else {
218 | 		logger.Debug("connect the grpc server successfully")
219 | 		success = true
220 | 	}
221 | 
222 | 	return
223 | }
224 | 


--------------------------------------------------------------------------------
/prober/grpc_test.go:
--------------------------------------------------------------------------------
  1 | // Copyright 2021 The Prometheus Authors
  2 | // Licensed under the Apache License, Version 2.0 (the "License");
  3 | // you may not use this file except in compliance with the License.
  4 | // You may obtain a copy of the License at
  5 | //
  6 | // http://www.apache.org/licenses/LICENSE-2.0
  7 | //
  8 | // Unless required by applicable law or agreed to in writing, software
  9 | // distributed under the License is distributed on an "AS IS" BASIS,
 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 11 | // See the License for the specific language governing permissions and
 12 | // limitations under the License.
 13 | 
 14 | package prober
 15 | 
 16 | import (
 17 | 	"context"
 18 | 	"crypto/tls"
 19 | 	"crypto/x509"
 20 | 	"encoding/pem"
 21 | 	"fmt"
 22 | 	"net"
 23 | 	"os"
 24 | 	"testing"
 25 | 	"time"
 26 | 
 27 | 	"github.com/prometheus/blackbox_exporter/config"
 28 | 	"github.com/prometheus/client_golang/prometheus"
 29 | 	pconfig "github.com/prometheus/common/config"
 30 | 	"github.com/prometheus/common/promslog"
 31 | 	"google.golang.org/grpc"
 32 | 	"google.golang.org/grpc/credentials"
 33 | 	"google.golang.org/grpc/health"
 34 | 	"google.golang.org/grpc/health/grpc_health_v1"
 35 | )
 36 | 
 37 | func TestGRPCConnection(t *testing.T) {
 38 | 	if os.Getenv("CI") == "true" {
 39 | 		t.Skip("skipping; CI is failing on ipv6 dns requests")
 40 | 	}
 41 | 
 42 | 	ln, err := net.Listen("tcp", "localhost:0")
 43 | 	if err != nil {
 44 | 		t.Fatalf("Error listening on socket: %s", err)
 45 | 	}
 46 | 	defer ln.Close()
 47 | 
 48 | 	_, port, err := net.SplitHostPort(ln.Addr().String())
 49 | 	if err != nil {
 50 | 		t.Fatalf("Error retrieving port for socket: %s", err)
 51 | 	}
 52 | 	s := grpc.NewServer()
 53 | 	healthServer := health.NewServer()
 54 | 	healthServer.SetServingStatus("service", grpc_health_v1.HealthCheckResponse_SERVING)
 55 | 	grpc_health_v1.RegisterHealthServer(s, healthServer)
 56 | 
 57 | 	go func() {
 58 | 		if err := s.Serve(ln); err != nil {
 59 | 			t.Errorf("failed to serve: %v", err)
 60 | 			return
 61 | 		}
 62 | 	}()
 63 | 	defer s.GracefulStop()
 64 | 
 65 | 	testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 66 | 	defer cancel()
 67 | 	registry := prometheus.NewRegistry()
 68 | 
 69 | 	result := ProbeGRPC(testCTX, "localhost:"+port,
 70 | 		config.Module{Timeout: time.Second, GRPC: config.GRPCProbe{
 71 | 			IPProtocolFallback: false,
 72 | 		},
 73 | 		}, registry, promslog.NewNopLogger())
 74 | 
 75 | 	if !result {
 76 | 		t.Fatalf("GRPC probe failed")
 77 | 	}
 78 | 
 79 | 	mfs, err := registry.Gather()
 80 | 	if err != nil {
 81 | 		t.Fatal(err)
 82 | 	}
 83 | 
 84 | 	expectedMetrics := map[string]map[string]map[string]struct{}{
 85 | 		"probe_grpc_healthcheck_response": {
 86 | 			"serving_status": {
 87 | 				"UNKNOWN":         {},
 88 | 				"SERVING":         {},
 89 | 				"NOT_SERVING":     {},
 90 | 				"SERVICE_UNKNOWN": {},
 91 | 			},
 92 | 		},
 93 | 	}
 94 | 
 95 | 	checkMetrics(expectedMetrics, mfs, t)
 96 | 
 97 | 	expectedResults := map[string]float64{
 98 | 		"probe_grpc_ssl":         0,
 99 | 		"probe_grpc_status_code": 0,
100 | 	}
101 | 
102 | 	checkRegistryResults(expectedResults, mfs, t)
103 | }
104 | 
105 | func TestMultipleGRPCservices(t *testing.T) {
106 | 	if os.Getenv("CI") == "true" {
107 | 		t.Skip("skipping; CI is failing on ipv6 dns requests")
108 | 	}
109 | 
110 | 	ln, err := net.Listen("tcp", "localhost:0")
111 | 	if err != nil {
112 | 		t.Fatalf("Error listening on socket: %s", err)
113 | 	}
114 | 	defer ln.Close()
115 | 
116 | 	_, port, err := net.SplitHostPort(ln.Addr().String())
117 | 	if err != nil {
118 | 		t.Fatalf("Error retrieving port for socket: %s", err)
119 | 	}
120 | 	s := grpc.NewServer()
121 | 	healthServer := health.NewServer()
122 | 	healthServer.SetServingStatus("service1", grpc_health_v1.HealthCheckResponse_SERVING)
123 | 	healthServer.SetServingStatus("service2", grpc_health_v1.HealthCheckResponse_NOT_SERVING)
124 | 	grpc_health_v1.RegisterHealthServer(s, healthServer)
125 | 
126 | 	go func() {
127 | 		if err := s.Serve(ln); err != nil {
128 | 			t.Errorf("failed to serve: %v", err)
129 | 			return
130 | 		}
131 | 	}()
132 | 	defer s.GracefulStop()
133 | 
134 | 	testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second)
135 | 	defer cancel()
136 | 	registryService1 := prometheus.NewRegistry()
137 | 
138 | 	resultService1 := ProbeGRPC(testCTX, "localhost:"+port,
139 | 		config.Module{Timeout: time.Second, GRPC: config.GRPCProbe{
140 | 			IPProtocolFallback: false,
141 | 			Service:            "service1",
142 | 		},
143 | 		}, registryService1, promslog.NewNopLogger())
144 | 
145 | 	if !resultService1 {
146 | 		t.Fatalf("GRPC probe failed for service1")
147 | 	}
148 | 
149 | 	registryService2 := prometheus.NewRegistry()
150 | 	resultService2 := ProbeGRPC(testCTX, "localhost:"+port,
151 | 		config.Module{Timeout: time.Second, GRPC: config.GRPCProbe{
152 | 			IPProtocolFallback: false,
153 | 			Service:            "service2",
154 | 		},
155 | 		}, registryService2, promslog.NewNopLogger())
156 | 
157 | 	if resultService2 {
158 | 		t.Fatalf("GRPC probe succeed for service2")
159 | 	}
160 | 
161 | 	registryService3 := prometheus.NewRegistry()
162 | 	resultService3 := ProbeGRPC(testCTX, "localhost:"+port,
163 | 		config.Module{Timeout: time.Second, GRPC: config.GRPCProbe{
164 | 			IPProtocolFallback: false,
165 | 			Service:            "service3",
166 | 		},
167 | 		}, registryService3, promslog.NewNopLogger())
168 | 
169 | 	if resultService3 {
170 | 		t.Fatalf("GRPC probe succeed for service3")
171 | 	}
172 | }
173 | 
174 | func TestGRPCTLSConnection(t *testing.T) {
175 | 	if os.Getenv("CI") == "true" {
176 | 		t.Skip("skipping; CI is failing on ipv6 dns requests")
177 | 	}
178 | 
179 | 	certExpiry := time.Now().AddDate(0, 0, 1)
180 | 	testCertTmpl := generateCertificateTemplate(certExpiry, false)
181 | 	testCertTmpl.IsCA = true
182 | 	_, testcertPem, testKey := generateSelfSignedCertificate(testCertTmpl)
183 | 
184 | 	// CAFile must be passed via filesystem, use a tempfile.
185 | 	tmpCaFile, err := os.CreateTemp("", "cafile.pem")
186 | 	if err != nil {
187 | 		t.Fatalf("Error creating CA tempfile: %s", err)
188 | 	}
189 | 	if _, err = tmpCaFile.Write(testcertPem); err != nil {
190 | 		t.Fatalf("Error writing CA tempfile: %s", err)
191 | 	}
192 | 	if err = tmpCaFile.Close(); err != nil {
193 | 		t.Fatalf("Error closing CA tempfile: %s", err)
194 | 	}
195 | 	defer os.Remove(tmpCaFile.Name())
196 | 
197 | 	testKeyPem := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(testKey)})
198 | 	testcert, err := tls.X509KeyPair(testcertPem, testKeyPem)
199 | 	if err != nil {
200 | 		panic(fmt.Sprintf("Failed to decode TLS testing keypair: %s\n", err))
201 | 	}
202 | 
203 | 	tlsConfig := &tls.Config{
204 | 		Certificates: []tls.Certificate{testcert},
205 | 		MinVersion:   tls.VersionTLS12,
206 | 		MaxVersion:   tls.VersionTLS12,
207 | 	}
208 | 
209 | 	ln, err := net.Listen("tcp", "localhost:0")
210 | 	if err != nil {
211 | 		t.Fatalf("Error listening on socket: %s", err)
212 | 	}
213 | 	defer ln.Close()
214 | 
215 | 	_, port, err := net.SplitHostPort(ln.Addr().String())
216 | 	if err != nil {
217 | 		t.Fatalf("Error retrieving port for socket: %s", err)
218 | 	}
219 | 
220 | 	s := grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsConfig)))
221 | 	healthServer := health.NewServer()
222 | 	healthServer.SetServingStatus("service", grpc_health_v1.HealthCheckResponse_SERVING)
223 | 	grpc_health_v1.RegisterHealthServer(s, healthServer)
224 | 
225 | 	go func() {
226 | 		if err := s.Serve(ln); err != nil {
227 | 			t.Errorf("failed to serve: %v", err)
228 | 			return
229 | 		}
230 | 	}()
231 | 	defer s.GracefulStop()
232 | 
233 | 	testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second)
234 | 	defer cancel()
235 | 	registry := prometheus.NewRegistry()
236 | 
237 | 	result := ProbeGRPC(testCTX, "localhost:"+port,
238 | 		config.Module{Timeout: time.Second, GRPC: config.GRPCProbe{
239 | 			TLS:                true,
240 | 			TLSConfig:          pconfig.TLSConfig{InsecureSkipVerify: true},
241 | 			IPProtocolFallback: false,
242 | 		},
243 | 		}, registry, promslog.NewNopLogger())
244 | 
245 | 	if !result {
246 | 		t.Fatalf("GRPC probe failed")
247 | 	}
248 | 
249 | 	mfs, err := registry.Gather()
250 | 	if err != nil {
251 | 		t.Fatal(err)
252 | 	}
253 | 
254 | 	expectedLabels := map[string]map[string]string{
255 | 		"probe_tls_version_info": {
256 | 			"version": "TLS 1.2",
257 | 		},
258 | 	}
259 | 	checkRegistryLabels(expectedLabels, mfs, t)
260 | 
261 | 	expectedResults := map[string]float64{
262 | 		"probe_grpc_ssl":         1,
263 | 		"probe_grpc_status_code": 0,
264 | 	}
265 | 
266 | 	checkRegistryResults(expectedResults, mfs, t)
267 | }
268 | 
269 | func TestNoTLSConnection(t *testing.T) {
270 | 	if os.Getenv("CI") == "true" {
271 | 		t.Skip("skipping; CI is failing on ipv6 dns requests")
272 | 	}
273 | 
274 | 	ln, err := net.Listen("tcp", "localhost:0")
275 | 	if err != nil {
276 | 		t.Fatalf("Error listening on socket: %s", err)
277 | 	}
278 | 	defer ln.Close()
279 | 
280 | 	_, port, err := net.SplitHostPort(ln.Addr().String())
281 | 	if err != nil {
282 | 		t.Fatalf("Error retrieving port for socket: %s", err)
283 | 	}
284 | 	s := grpc.NewServer()
285 | 	healthServer := health.NewServer()
286 | 	healthServer.SetServingStatus("service", grpc_health_v1.HealthCheckResponse_SERVING)
287 | 	grpc_health_v1.RegisterHealthServer(s, healthServer)
288 | 
289 | 	go func() {
290 | 		if err := s.Serve(ln); err != nil {
291 | 			t.Errorf("failed to serve: %v", err)
292 | 			return
293 | 		}
294 | 	}()
295 | 	defer s.GracefulStop()
296 | 
297 | 	testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second)
298 | 	defer cancel()
299 | 	registry := prometheus.NewRegistry()
300 | 
301 | 	result := ProbeGRPC(testCTX, "localhost:"+port,
302 | 		config.Module{Timeout: time.Second, GRPC: config.GRPCProbe{
303 | 			TLS:                true,
304 | 			TLSConfig:          pconfig.TLSConfig{InsecureSkipVerify: true},
305 | 			IPProtocolFallback: false,
306 | 		},
307 | 		}, registry, promslog.NewNopLogger())
308 | 
309 | 	if result {
310 | 		t.Fatalf("GRPC probe succeed")
311 | 	}
312 | 
313 | 	mfs, err := registry.Gather()
314 | 	if err != nil {
315 | 		t.Fatal(err)
316 | 	}
317 | 
318 | 	expectedResults := map[string]float64{
319 | 		"probe_grpc_ssl":         0,
320 | 		"probe_grpc_status_code": 14, // UNAVAILABLE
321 | 	}
322 | 
323 | 	checkRegistryResults(expectedResults, mfs, t)
324 | 
325 | }
326 | 
327 | func TestGRPCServiceNotFound(t *testing.T) {
328 | 	if os.Getenv("CI") == "true" {
329 | 		t.Skip("skipping; CI is failing on ipv6 dns requests")
330 | 	}
331 | 
332 | 	ln, err := net.Listen("tcp", "localhost:0")
333 | 	if err != nil {
334 | 		t.Fatalf("Error listening on socket: %s", err)
335 | 	}
336 | 	defer ln.Close()
337 | 
338 | 	_, port, err := net.SplitHostPort(ln.Addr().String())
339 | 	if err != nil {
340 | 		t.Fatalf("Error retrieving port for socket: %s", err)
341 | 	}
342 | 	s := grpc.NewServer()
343 | 	healthServer := health.NewServer()
344 | 	healthServer.SetServingStatus("service", grpc_health_v1.HealthCheckResponse_SERVING)
345 | 	grpc_health_v1.RegisterHealthServer(s, healthServer)
346 | 
347 | 	go func() {
348 | 		if err := s.Serve(ln); err != nil {
349 | 			t.Errorf("failed to serve: %v", err)
350 | 			return
351 | 		}
352 | 	}()
353 | 	defer s.GracefulStop()
354 | 
355 | 	testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second)
356 | 	defer cancel()
357 | 	registry := prometheus.NewRegistry()
358 | 
359 | 	result := ProbeGRPC(testCTX, "localhost:"+port,
360 | 		config.Module{Timeout: time.Second, GRPC: config.GRPCProbe{
361 | 			IPProtocolFallback: false,
362 | 			Service:            "NonExistingService",
363 | 		},
364 | 		}, registry, promslog.NewNopLogger())
365 | 
366 | 	if result {
367 | 		t.Fatalf("GRPC probe succeed")
368 | 	}
369 | 
370 | 	mfs, err := registry.Gather()
371 | 	if err != nil {
372 | 		t.Fatal(err)
373 | 	}
374 | 
375 | 	expectedResults := map[string]float64{
376 | 		"probe_grpc_ssl":         0,
377 | 		"probe_grpc_status_code": 5, // NOT_FOUND
378 | 	}
379 | 
380 | 	checkRegistryResults(expectedResults, mfs, t)
381 | }
382 | 
383 | func TestGRPCHealthCheckUnimplemented(t *testing.T) {
384 | 	if os.Getenv("CI") == "true" {
385 | 		t.Skip("skipping; CI is failing on ipv6 dns requests")
386 | 	}
387 | 
388 | 	ln, err := net.Listen("tcp", "localhost:0")
389 | 	if err != nil {
390 | 		t.Fatalf("Error listening on socket: %s", err)
391 | 	}
392 | 	defer ln.Close()
393 | 
394 | 	_, port, err := net.SplitHostPort(ln.Addr().String())
395 | 	if err != nil {
396 | 		t.Fatalf("Error retrieving port for socket: %s", err)
397 | 	}
398 | 	s := grpc.NewServer()
399 | 
400 | 	go func() {
401 | 		if err := s.Serve(ln); err != nil {
402 | 			t.Errorf("failed to serve: %v", err)
403 | 			return
404 | 		}
405 | 	}()
406 | 	defer s.GracefulStop()
407 | 
408 | 	testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second)
409 | 	defer cancel()
410 | 	registry := prometheus.NewRegistry()
411 | 
412 | 	result := ProbeGRPC(testCTX, "localhost:"+port,
413 | 		config.Module{Timeout: time.Second, GRPC: config.GRPCProbe{
414 | 			IPProtocolFallback: false,
415 | 			Service:            "NonExistingService",
416 | 		},
417 | 		}, registry, promslog.NewNopLogger())
418 | 
419 | 	if result {
420 | 		t.Fatalf("GRPC probe succeed")
421 | 	}
422 | 
423 | 	mfs, err := registry.Gather()
424 | 	if err != nil {
425 | 		t.Fatal(err)
426 | 	}
427 | 
428 | 	expectedResults := map[string]float64{
429 | 		"probe_grpc_ssl":         0,
430 | 		"probe_grpc_status_code": 12, // UNIMPLEMENTED
431 | 	}
432 | 
433 | 	checkRegistryResults(expectedResults, mfs, t)
434 | }
435 | 
436 | func TestGRPCAbsentFailedTLS(t *testing.T) {
437 | 	testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second)
438 | 	defer cancel()
439 | 	registry := prometheus.NewRegistry()
440 | 
441 | 	// probe and invalid port to trigger TCP/TLS error
442 | 	result := ProbeGRPC(testCTX, "localhost:0",
443 | 		config.Module{Timeout: time.Second, GRPC: config.GRPCProbe{
444 | 			IPProtocolFallback: false,
445 | 			Service:            "NonExistingService",
446 | 		},
447 | 		}, registry, promslog.NewNopLogger())
448 | 
449 | 	if result {
450 | 		t.Fatalf("GRPC probe succeeded, should have failed")
451 | 	}
452 | 
453 | 	mfs, err := registry.Gather()
454 | 	if err != nil {
455 | 		t.Fatal(err)
456 | 	}
457 | 
458 | 	absentMetrics := []string{
459 | 		"probe_ssl_earliest_cert_expiry",
460 | 		"probe_tls_version_info",
461 | 		"probe_ssl_last_chain_info",
462 | 	}
463 | 
464 | 	checkAbsentMetrics(absentMetrics, mfs, t)
465 | }
466 | 


--------------------------------------------------------------------------------
/prober/handler.go:
--------------------------------------------------------------------------------
  1 | // Copyright 2016 The Prometheus Authors
  2 | // Licensed under the Apache License, Version 2.0 (the "License");
  3 | // you may not use this file except in compliance with the License.
  4 | // You may obtain a copy of the License at
  5 | //
  6 | // http://www.apache.org/licenses/LICENSE-2.0
  7 | //
  8 | // Unless required by applicable law or agreed to in writing, software
  9 | // distributed under the License is distributed on an "AS IS" BASIS,
 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 11 | // See the License for the specific language governing permissions and
 12 | // limitations under the License.
 13 | 
 14 | package prober
 15 | 
 16 | import (
 17 | 	"bytes"
 18 | 	"context"
 19 | 	"fmt"
 20 | 	"log/slog"
 21 | 	"net/http"
 22 | 	"net/textproto"
 23 | 	"net/url"
 24 | 	"strconv"
 25 | 	"time"
 26 | 
 27 | 	"github.com/prometheus/blackbox_exporter/config"
 28 | 	"github.com/prometheus/client_golang/prometheus"
 29 | 	"github.com/prometheus/client_golang/prometheus/promhttp"
 30 | 	"github.com/prometheus/common/expfmt"
 31 | 	"github.com/prometheus/common/promslog"
 32 | 	"gopkg.in/yaml.v2"
 33 | )
 34 | 
 35 | var (
 36 | 	Probers = map[string]ProbeFn{
 37 | 		"http": ProbeHTTP,
 38 | 		"tcp":  ProbeTCP,
 39 | 		"icmp": ProbeICMP,
 40 | 		"dns":  ProbeDNS,
 41 | 		"grpc": ProbeGRPC,
 42 | 	}
 43 | )
 44 | 
 45 | func Handler(w http.ResponseWriter, r *http.Request, c *config.Config, logger *slog.Logger, rh *ResultHistory, timeoutOffset float64, params url.Values,
 46 | 	moduleUnknownCounter prometheus.Counter,
 47 | 	logLevel, logLevelProber *promslog.Level) {
 48 | 
 49 | 	if params == nil {
 50 | 		params = r.URL.Query()
 51 | 	}
 52 | 	moduleName := params.Get("module")
 53 | 	if moduleName == "" {
 54 | 		moduleName = "http_2xx"
 55 | 	}
 56 | 	module, ok := c.Modules[moduleName]
 57 | 	if !ok {
 58 | 		http.Error(w, fmt.Sprintf("Unknown module %q", moduleName), http.StatusBadRequest)
 59 | 		logger.Debug("Unknown module", "module", moduleName)
 60 | 		if moduleUnknownCounter != nil {
 61 | 			moduleUnknownCounter.Add(1)
 62 | 		}
 63 | 		return
 64 | 	}
 65 | 
 66 | 	timeoutSeconds, err := getTimeout(r, module, timeoutOffset)
 67 | 	if err != nil {
 68 | 		http.Error(w, fmt.Sprintf("Failed to parse timeout from Prometheus header: %s", err), http.StatusInternalServerError)
 69 | 		return
 70 | 	}
 71 | 
 72 | 	ctx, cancel := context.WithTimeout(r.Context(), time.Duration(timeoutSeconds*float64(time.Second)))
 73 | 	defer cancel()
 74 | 	r = r.WithContext(ctx)
 75 | 
 76 | 	probeSuccessGauge := prometheus.NewGauge(prometheus.GaugeOpts{
 77 | 		Name: "probe_success",
 78 | 		Help: "Displays whether or not the probe was a success",
 79 | 	})
 80 | 	probeDurationGauge := prometheus.NewGauge(prometheus.GaugeOpts{
 81 | 		Name: "probe_duration_seconds",
 82 | 		Help: "Returns how long the probe took to complete in seconds",
 83 | 	})
 84 | 
 85 | 	target := params.Get("target")
 86 | 	if target == "" {
 87 | 		http.Error(w, "Target parameter is missing", http.StatusBadRequest)
 88 | 		return
 89 | 	}
 90 | 
 91 | 	prober, ok := Probers[module.Prober]
 92 | 	if !ok {
 93 | 		http.Error(w, fmt.Sprintf("Unknown prober %q", module.Prober), http.StatusBadRequest)
 94 | 		return
 95 | 	}
 96 | 
 97 | 	hostname := params.Get("hostname")
 98 | 	if module.Prober == "http" && hostname != "" {
 99 | 		err = setHTTPHost(hostname, &module)
100 | 		if err != nil {
101 | 			http.Error(w, err.Error(), http.StatusBadRequest)
102 | 			return
103 | 		}
104 | 	}
105 | 
106 | 	if module.Prober == "tcp" && hostname != "" {
107 | 		if module.TCP.TLSConfig.ServerName == "" {
108 | 			module.TCP.TLSConfig.ServerName = hostname
109 | 		}
110 | 	}
111 | 
112 | 	sl := newScrapeLogger(logger, moduleName, target, logLevel, logLevelProber)
113 | 	slLogger := slog.New(sl)
114 | 
115 | 	slLogger.Info("Beginning probe", "probe", module.Prober, "timeout_seconds", timeoutSeconds)
116 | 
117 | 	start := time.Now()
118 | 	registry := prometheus.NewRegistry()
119 | 	registry.MustRegister(probeSuccessGauge)
120 | 	registry.MustRegister(probeDurationGauge)
121 | 	success := prober(ctx, target, module, registry, slLogger)
122 | 	duration := time.Since(start).Seconds()
123 | 	probeDurationGauge.Set(duration)
124 | 	if success {
125 | 		probeSuccessGauge.Set(1)
126 | 		slLogger.Info("Probe succeeded", "duration_seconds", duration)
127 | 	} else {
128 | 		slLogger.Error("Probe failed", "duration_seconds", duration)
129 | 	}
130 | 
131 | 	debugOutput := DebugOutput(&module, &sl.buffer, registry)
132 | 	rh.Add(moduleName, target, debugOutput, success)
133 | 
134 | 	if r.URL.Query().Get("debug") == "true" {
135 | 		w.Header().Set("Content-Type", "text/plain")
136 | 		w.Write([]byte(debugOutput))
137 | 		return
138 | 	}
139 | 
140 | 	h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{})
141 | 	h.ServeHTTP(w, r)
142 | }
143 | 
144 | func setHTTPHost(hostname string, module *config.Module) error {
145 | 	// By creating a new hashmap and copying values there we
146 | 	// ensure that the initial configuration remain intact.
147 | 	headers := make(map[string]string)
148 | 	if module.HTTP.Headers != nil {
149 | 		for name, value := range module.HTTP.Headers {
150 | 			if textproto.CanonicalMIMEHeaderKey(name) == "Host" && value != hostname {
151 | 				return fmt.Errorf("host header defined both in module configuration (%s) and with URL-parameter 'hostname' (%s)", value, hostname)
152 | 			}
153 | 			headers[name] = value
154 | 		}
155 | 	}
156 | 	headers["Host"] = hostname
157 | 	module.HTTP.Headers = headers
158 | 	return nil
159 | }
160 | 
161 | type scrapeLogger struct {
162 | 	next           *slog.Logger
163 | 	buffer         bytes.Buffer
164 | 	bufferLogger   *slog.Logger
165 | 	logLevelProber *promslog.Level
166 | }
167 | 
168 | // Enabled returns true if both A) the scrapeLogger's internal `next` logger
169 | // and B) the scrapeLogger's internal `bufferLogger` are enabled at the
170 | // provided context/log level, and returns false otherwise. It implements
171 | // slog.Handler.
172 | func (sl *scrapeLogger) Enabled(ctx context.Context, level slog.Level) bool {
173 | 	nextEnabled := sl.next.Enabled(ctx, level)
174 | 	bufEnabled := sl.bufferLogger.Enabled(ctx, level)
175 | 
176 | 	return nextEnabled && bufEnabled
177 | }
178 | 
179 | // Handle writes the provided log record to the internal logger, and then to
180 | // the internal bufferLogger for use with serving debug output. It implements
181 | // slog.Handler.
182 | func (sl *scrapeLogger) Handle(ctx context.Context, r slog.Record) error {
183 | 	level := getSlogLevel(sl.logLevelProber.String())
184 | 
185 | 	// Collect attributes from record so we can log them directly. We
186 | 	// hijack log calls to the scrapeLogger and override the level from the
187 | 	// original log call with the level set via the `--log.prober` flag.
188 | 	attrs := make([]slog.Attr, r.NumAttrs())
189 | 	r.Attrs(func(a slog.Attr) bool {
190 | 		attrs = append(attrs, a)
191 | 		return true
192 | 	})
193 | 
194 | 	sl.next.LogAttrs(ctx, level, r.Message, attrs...)
195 | 	sl.bufferLogger.LogAttrs(ctx, level, r.Message, attrs...)
196 | 
197 | 	return nil
198 | }
199 | 
200 | // WithAttrs adds the provided attributes to the scrapeLogger's internal logger and
201 | // bufferLogger. It implements slog.Handler.
202 | func (sl *scrapeLogger) WithAttrs(attrs []slog.Attr) slog.Handler {
203 | 	return &scrapeLogger{
204 | 		next:           slog.New(sl.next.Handler().WithAttrs(attrs)),
205 | 		buffer:         sl.buffer,
206 | 		bufferLogger:   slog.New(sl.bufferLogger.Handler().WithAttrs(attrs)),
207 | 		logLevelProber: sl.logLevelProber,
208 | 	}
209 | }
210 | 
211 | // WithGroup adds the provided group name to the scrapeLogger's internal logger
212 | // and bufferLogger. It implements slog.Handler.
213 | func (sl *scrapeLogger) WithGroup(name string) slog.Handler {
214 | 	return &scrapeLogger{
215 | 		next:           slog.New(sl.next.Handler().WithGroup(name)),
216 | 		buffer:         sl.buffer,
217 | 		bufferLogger:   slog.New(sl.bufferLogger.Handler().WithGroup(name)),
218 | 		logLevelProber: sl.logLevelProber,
219 | 	}
220 | }
221 | 
222 | func newScrapeLogger(logger *slog.Logger, module string, target string, logLevel, logLevelProber *promslog.Level) *scrapeLogger {
223 | 	if logLevelProber == nil {
224 | 		logLevelProber = promslog.NewLevel()
225 | 	}
226 | 	sl := &scrapeLogger{
227 | 		next:           logger.With("module", module, "target", target),
228 | 		buffer:         bytes.Buffer{},
229 | 		logLevelProber: logLevelProber,
230 | 	}
231 | 	bl := promslog.New(&promslog.Config{Writer: &sl.buffer, Level: logLevel})
232 | 	sl.bufferLogger = bl.With("module", module, "target", target)
233 | 	return sl
234 | }
235 | 
236 | func getSlogLevel(level string) slog.Level {
237 | 	switch level {
238 | 	case "info":
239 | 		return slog.LevelInfo
240 | 	case "debug":
241 | 		return slog.LevelDebug
242 | 	case "error":
243 | 		return slog.LevelError
244 | 	case "warn":
245 | 		return slog.LevelWarn
246 | 	default:
247 | 		return slog.LevelInfo
248 | 	}
249 | }
250 | 
251 | // DebugOutput returns plaintext debug output for a probe.
252 | func DebugOutput(module *config.Module, logBuffer *bytes.Buffer, registry *prometheus.Registry) string {
253 | 	buf := &bytes.Buffer{}
254 | 	fmt.Fprintf(buf, "Logs for the probe:\n")
255 | 	logBuffer.WriteTo(buf)
256 | 	fmt.Fprintf(buf, "\n\n\nMetrics that would have been returned:\n")
257 | 	mfs, err := registry.Gather()
258 | 	if err != nil {
259 | 		fmt.Fprintf(buf, "Error gathering metrics: %s\n", err)
260 | 	}
261 | 	for _, mf := range mfs {
262 | 		expfmt.MetricFamilyToText(buf, mf)
263 | 	}
264 | 	fmt.Fprintf(buf, "\n\n\nModule configuration:\n")
265 | 	c, err := yaml.Marshal(module)
266 | 	if err != nil {
267 | 		fmt.Fprintf(buf, "Error marshalling config: %s\n", err)
268 | 	}
269 | 	buf.Write(c)
270 | 
271 | 	return buf.String()
272 | }
273 | 
274 | func getTimeout(r *http.Request, module config.Module, offset float64) (timeoutSeconds float64, err error) {
275 | 	// If a timeout is configured via the Prometheus header, add it to the request.
276 | 	if v := r.Header.Get("X-Prometheus-Scrape-Timeout-Seconds"); v != "" {
277 | 		var err error
278 | 		timeoutSeconds, err = strconv.ParseFloat(v, 64)
279 | 		if err != nil {
280 | 			return 0, err
281 | 		}
282 | 	}
283 | 	if timeoutSeconds == 0 {
284 | 		timeoutSeconds = 120
285 | 	}
286 | 
287 | 	var maxTimeoutSeconds = timeoutSeconds - offset
288 | 	if module.Timeout.Seconds() < maxTimeoutSeconds && module.Timeout.Seconds() > 0 || maxTimeoutSeconds < 0 {
289 | 		timeoutSeconds = module.Timeout.Seconds()
290 | 	} else {
291 | 		timeoutSeconds = maxTimeoutSeconds
292 | 	}
293 | 
294 | 	return timeoutSeconds, nil
295 | }
296 | 


--------------------------------------------------------------------------------
/prober/handler_test.go:
--------------------------------------------------------------------------------
  1 | // Copyright 2016 The Prometheus Authors
  2 | // Licensed under the Apache License, Version 2.0 (the "License");
  3 | // you may not use this file except in compliance with the License.
  4 | // You may obtain a copy of the License at
  5 | //
  6 | // http://www.apache.org/licenses/LICENSE-2.0
  7 | //
  8 | // Unless required by applicable law or agreed to in writing, software
  9 | // distributed under the License is distributed on an "AS IS" BASIS,
 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 11 | // See the License for the specific language governing permissions and
 12 | // limitations under the License.
 13 | 
 14 | package prober
 15 | 
 16 | import (
 17 | 	"bytes"
 18 | 	"fmt"
 19 | 	"net"
 20 | 	"net/http"
 21 | 	"net/http/httptest"
 22 | 	"strconv"
 23 | 	"strings"
 24 | 	"testing"
 25 | 	"time"
 26 | 
 27 | 	"github.com/prometheus/client_golang/prometheus"
 28 | 	pconfig "github.com/prometheus/common/config"
 29 | 	"github.com/prometheus/common/promslog"
 30 | 
 31 | 	"github.com/prometheus/blackbox_exporter/config"
 32 | )
 33 | 
 34 | var c = &config.Config{
 35 | 	Modules: map[string]config.Module{
 36 | 		"http_2xx": {
 37 | 			Prober:  "http",
 38 | 			Timeout: 10 * time.Second,
 39 | 			HTTP: config.HTTPProbe{
 40 | 				HTTPClientConfig: pconfig.HTTPClientConfig{
 41 | 					BearerToken: "mysecret",
 42 | 				},
 43 | 			},
 44 | 		},
 45 | 	},
 46 | }
 47 | 
 48 | func TestPrometheusTimeoutHTTP(t *testing.T) {
 49 | 	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 50 | 		time.Sleep(2 * time.Second)
 51 | 	}))
 52 | 	defer ts.Close()
 53 | 
 54 | 	req, err := http.NewRequest("GET", "?target="+ts.URL, nil)
 55 | 	if err != nil {
 56 | 		t.Fatal(err)
 57 | 	}
 58 | 	req.Header.Set("X-Prometheus-Scrape-Timeout-Seconds", "1")
 59 | 
 60 | 	rr := httptest.NewRecorder()
 61 | 	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 62 | 		Handler(w, r, c, promslog.NewNopLogger(), &ResultHistory{}, 0.5, nil, nil, promslog.NewLevel(), promslog.NewLevel())
 63 | 	})
 64 | 
 65 | 	handler.ServeHTTP(rr, req)
 66 | 
 67 | 	if status := rr.Code; status != http.StatusOK {
 68 | 		t.Errorf("probe request handler returned wrong status code: %v, want %v", status, http.StatusOK)
 69 | 	}
 70 | }
 71 | 
 72 | func TestPrometheusConfigSecretsHidden(t *testing.T) {
 73 | 	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 74 | 		time.Sleep(2 * time.Second)
 75 | 	}))
 76 | 	defer ts.Close()
 77 | 
 78 | 	req, err := http.NewRequest("GET", "?debug=true&target="+ts.URL, nil)
 79 | 	if err != nil {
 80 | 		t.Fatal(err)
 81 | 	}
 82 | 	rr := httptest.NewRecorder()
 83 | 	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 84 | 		Handler(w, r, c, promslog.NewNopLogger(), &ResultHistory{}, 0.5, nil, nil, promslog.NewLevel(), promslog.NewLevel())
 85 | 	})
 86 | 	handler.ServeHTTP(rr, req)
 87 | 
 88 | 	body := rr.Body.String()
 89 | 	if strings.Contains(body, "mysecret") {
 90 | 		t.Errorf("Secret exposed in debug config output: %v", body)
 91 | 	}
 92 | 	if !strings.Contains(body, "<secret>") {
 93 | 		t.Errorf("Hidden secret missing from debug config output: %v", body)
 94 | 	}
 95 | }
 96 | 
 97 | func TestDebugOutputSecretsHidden(t *testing.T) {
 98 | 	module := c.Modules["http_2xx"]
 99 | 	out := DebugOutput(&module, &bytes.Buffer{}, prometheus.NewRegistry())
100 | 
101 | 	if strings.Contains(out, "mysecret") {
102 | 		t.Errorf("Secret exposed in debug output: %v", out)
103 | 	}
104 | 	if !strings.Contains(out, "<secret>") {
105 | 		t.Errorf("Hidden secret missing from debug output: %v", out)
106 | 	}
107 | }
108 | 
109 | func TestTimeoutIsSetCorrectly(t *testing.T) {
110 | 	var tests = []struct {
111 | 		inModuleTimeout     time.Duration
112 | 		inPrometheusTimeout string
113 | 		inOffset            float64
114 | 		outTimeout          float64
115 | 	}{
116 | 		{0 * time.Second, "15", 0.5, 14.5},
117 | 		{0 * time.Second, "15", 0, 15},
118 | 		{20 * time.Second, "15", 0.5, 14.5},
119 | 		{20 * time.Second, "15", 0, 15},
120 | 		{5 * time.Second, "15", 0, 5},
121 | 		{5 * time.Second, "15", 0.5, 5},
122 | 		{10 * time.Second, "", 0.5, 10},
123 | 		{10 * time.Second, "10", 0.5, 9.5},
124 | 		{9500 * time.Millisecond, "", 0.5, 9.5},
125 | 		{9500 * time.Millisecond, "", 1, 9.5},
126 | 		{0 * time.Second, "", 0.5, 119.5},
127 | 		{0 * time.Second, "", 0, 120},
128 | 	}
129 | 
130 | 	for _, v := range tests {
131 | 		request, _ := http.NewRequest("GET", "", nil)
132 | 		request.Header.Set("X-Prometheus-Scrape-Timeout-Seconds", v.inPrometheusTimeout)
133 | 		module := config.Module{
134 | 			Timeout: v.inModuleTimeout,
135 | 		}
136 | 
137 | 		timeout, _ := getTimeout(request, module, v.inOffset)
138 | 		if timeout != v.outTimeout {
139 | 			t.Errorf("timeout is incorrect: %v, want %v", timeout, v.outTimeout)
140 | 		}
141 | 	}
142 | }
143 | 
144 | func TestHostnameParam(t *testing.T) {
145 | 	headers := map[string]string{}
146 | 	c := &config.Config{
147 | 		Modules: map[string]config.Module{
148 | 			"http_2xx": {
149 | 				Prober:  "http",
150 | 				Timeout: 10 * time.Second,
151 | 				HTTP: config.HTTPProbe{
152 | 					Headers:            headers,
153 | 					IPProtocolFallback: true,
154 | 				},
155 | 			},
156 | 		},
157 | 	}
158 | 
159 | 	// check that 'hostname' parameter make its way to Host header
160 | 	hostname := "foo.example.com"
161 | 
162 | 	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
163 | 		if r.Host != hostname {
164 | 			t.Errorf("Unexpected Host: expected %q, got %q.", hostname, r.Host)
165 | 		}
166 | 		w.WriteHeader(http.StatusOK)
167 | 	}))
168 | 	defer ts.Close()
169 | 
170 | 	requrl := fmt.Sprintf("?debug=true&hostname=%s&target=%s", hostname, ts.URL)
171 | 
172 | 	req, err := http.NewRequest("GET", requrl, nil)
173 | 	if err != nil {
174 | 		t.Fatal(err)
175 | 	}
176 | 
177 | 	rr := httptest.NewRecorder()
178 | 
179 | 	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
180 | 		Handler(w, r, c, promslog.NewNopLogger(), &ResultHistory{}, 0.5, nil, nil, promslog.NewLevel(), promslog.NewLevel())
181 | 	})
182 | 
183 | 	handler.ServeHTTP(rr, req)
184 | 
185 | 	if status := rr.Code; status != http.StatusOK {
186 | 		t.Errorf("probe request handler returned wrong status code: %v, want %v", status, http.StatusOK)
187 | 	}
188 | 
189 | 	// check that ts got the request to perform header check
190 | 	if !strings.Contains(rr.Body.String(), "probe_success 1") {
191 | 		t.Errorf("probe failed, response body: %v", rr.Body.String())
192 | 	}
193 | 
194 | 	// check that host header both in config and in parameter will result in 400
195 | 	c.Modules["http_2xx"].HTTP.Headers["Host"] = hostname + ".something"
196 | 
197 | 	handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
198 | 		Handler(w, r, c, promslog.NewNopLogger(), &ResultHistory{}, 0.5, nil, nil, promslog.NewLevel(), promslog.NewLevel())
199 | 	})
200 | 
201 | 	rr = httptest.NewRecorder()
202 | 	handler.ServeHTTP(rr, req)
203 | 
204 | 	if status := rr.Code; status != http.StatusBadRequest {
205 | 		t.Errorf("probe request handler returned wrong status code: %v, want %v", status, http.StatusBadRequest)
206 | 	}
207 | }
208 | 
209 | func TestTCPHostnameParam(t *testing.T) {
210 | 	c := &config.Config{
211 | 		Modules: map[string]config.Module{
212 | 			"tls_connect": {
213 | 				Prober:  "tcp",
214 | 				Timeout: 10 * time.Second,
215 | 				TCP: config.TCPProbe{
216 | 					TLS:        true,
217 | 					IPProtocol: "ip4",
218 | 					TLSConfig:  pconfig.TLSConfig{InsecureSkipVerify: true},
219 | 				},
220 | 			},
221 | 		},
222 | 	}
223 | 
224 | 	// check that 'hostname' parameter make its way to server_name in the tls_config
225 | 	hostname := "foo.example.com"
226 | 
227 | 	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
228 | 		if r.Host != hostname {
229 | 			t.Errorf("Unexpected Host: expected %q, got %q.", hostname, r.Host)
230 | 		}
231 | 		w.WriteHeader(http.StatusOK)
232 | 	}))
233 | 	defer ts.Close()
234 | 
235 | 	requrl := fmt.Sprintf("?module=tls_connect&debug=true&hostname=%s&target=%s", hostname, ts.Listener.Addr().(*net.TCPAddr).IP.String()+":"+strconv.Itoa(ts.Listener.Addr().(*net.TCPAddr).Port))
236 | 
237 | 	req, err := http.NewRequest("GET", requrl, nil)
238 | 	if err != nil {
239 | 		t.Fatal(err)
240 | 	}
241 | 
242 | 	rr := httptest.NewRecorder()
243 | 
244 | 	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
245 | 		Handler(w, r, c, promslog.NewNopLogger(), &ResultHistory{}, 0.5, nil, nil, promslog.NewLevel(), promslog.NewLevel())
246 | 	})
247 | 
248 | 	handler.ServeHTTP(rr, req)
249 | 
250 | 	if status := rr.Code; status != http.StatusOK {
251 | 		t.Errorf("probe request handler returned wrong status code: %v, want %v", status, http.StatusOK)
252 | 	}
253 | 
254 | 	// check debug output to confirm the server_name is set in tls_config and matches supplied hostname
255 | 	if !strings.Contains(rr.Body.String(), "server_name: "+hostname) {
256 | 		t.Errorf("probe failed, response body: %v", rr.Body.String())
257 | 	}
258 | 
259 | }
260 | 


--------------------------------------------------------------------------------
/prober/history.go:
--------------------------------------------------------------------------------
  1 | // Copyright 2017 The Prometheus Authors
  2 | // Licensed under the Apache License, Version 2.0 (the "License");
  3 | // you may not use this file except in compliance with the License.
  4 | // You may obtain a copy of the License at
  5 | //
  6 | // http://www.apache.org/licenses/LICENSE-2.0
  7 | //
  8 | // Unless required by applicable law or agreed to in writing, software
  9 | // distributed under the License is distributed on an "AS IS" BASIS,
 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 11 | // See the License for the specific language governing permissions and
 12 | // limitations under the License.
 13 | 
 14 | package prober
 15 | 
 16 | import (
 17 | 	"sync"
 18 | )
 19 | 
 20 | // Result contains the result of the execution of a probe
 21 | type Result struct {
 22 | 	Id          int64
 23 | 	ModuleName  string
 24 | 	Target      string
 25 | 	DebugOutput string
 26 | 	Success     bool
 27 | }
 28 | 
 29 | // ResultHistory contains two history slices: `results` contains most recent `maxResults` results.
 30 | // After they expire out of `results`, failures will be saved in `preservedFailedResults`. This
 31 | // ensures that we are always able to see debug information about recent failures.
 32 | type ResultHistory struct {
 33 | 	mu                     sync.Mutex
 34 | 	nextId                 int64
 35 | 	results                []*Result
 36 | 	preservedFailedResults []*Result
 37 | 	MaxResults             uint
 38 | }
 39 | 
 40 | // Add a result to the history.
 41 | func (rh *ResultHistory) Add(moduleName, target, debugOutput string, success bool) {
 42 | 	rh.mu.Lock()
 43 | 	defer rh.mu.Unlock()
 44 | 
 45 | 	r := &Result{
 46 | 		Id:          rh.nextId,
 47 | 		ModuleName:  moduleName,
 48 | 		Target:      target,
 49 | 		DebugOutput: debugOutput,
 50 | 		Success:     success,
 51 | 	}
 52 | 	rh.nextId++
 53 | 
 54 | 	rh.results = append(rh.results, r)
 55 | 	if uint(len(rh.results)) > rh.MaxResults {
 56 | 		// If we are about to remove a failure, add it to the failed result history, then
 57 | 		// remove the oldest failed result, if needed.
 58 | 		if !rh.results[0].Success {
 59 | 			rh.preservedFailedResults = append(rh.preservedFailedResults, rh.results[0])
 60 | 			if uint(len(rh.preservedFailedResults)) > rh.MaxResults {
 61 | 				preservedFailedResults := make([]*Result, len(rh.preservedFailedResults)-1)
 62 | 				copy(preservedFailedResults, rh.preservedFailedResults[1:])
 63 | 				rh.preservedFailedResults = preservedFailedResults
 64 | 			}
 65 | 		}
 66 | 		results := make([]*Result, len(rh.results)-1)
 67 | 		copy(results, rh.results[1:])
 68 | 		rh.results = results
 69 | 	}
 70 | }
 71 | 
 72 | // List returns a list of all results.
 73 | func (rh *ResultHistory) List() []*Result {
 74 | 	rh.mu.Lock()
 75 | 	defer rh.mu.Unlock()
 76 | 
 77 | 	// Results in each slice are disjoint. We can simply concatenate the results.
 78 | 	return append(rh.preservedFailedResults[:], rh.results...)
 79 | }
 80 | 
 81 | // Get returns a given result by id.
 82 | func (rh *ResultHistory) GetById(id int64) *Result {
 83 | 	rh.mu.Lock()
 84 | 	defer rh.mu.Unlock()
 85 | 
 86 | 	for _, r := range rh.preservedFailedResults {
 87 | 		if r.Id == id {
 88 | 			return r
 89 | 		}
 90 | 	}
 91 | 	for _, r := range rh.results {
 92 | 		if r.Id == id {
 93 | 			return r
 94 | 		}
 95 | 	}
 96 | 
 97 | 	return nil
 98 | }
 99 | 
100 | // Get returns a given result by url.
101 | func (rh *ResultHistory) GetByTarget(target string) *Result {
102 | 	rh.mu.Lock()
103 | 	defer rh.mu.Unlock()
104 | 
105 | 	for _, r := range rh.preservedFailedResults {
106 | 		if r.Target == target {
107 | 			return r
108 | 		}
109 | 	}
110 | 	for _, r := range rh.results {
111 | 		if r.Target == target {
112 | 			return r
113 | 		}
114 | 	}
115 | 
116 | 	return nil
117 | }
118 | 


--------------------------------------------------------------------------------
/prober/history_test.go:
--------------------------------------------------------------------------------
  1 | // Copyright 2017 The Prometheus Authors
  2 | // Licensed under the Apache License, Version 2.0 (the "License");
  3 | // you may not use this file except in compliance with the License.
  4 | // You may obtain a copy of the License at
  5 | //
  6 | // http://www.apache.org/licenses/LICENSE-2.0
  7 | //
  8 | // Unless required by applicable law or agreed to in writing, software
  9 | // distributed under the License is distributed on an "AS IS" BASIS,
 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 11 | // See the License for the specific language governing permissions and
 12 | // limitations under the License.
 13 | 
 14 | package prober
 15 | 
 16 | import (
 17 | 	"fmt"
 18 | 	"testing"
 19 | )
 20 | 
 21 | func TestHistoryKeepsLatestResults(t *testing.T) {
 22 | 	history := &ResultHistory{MaxResults: 3}
 23 | 	for i := 0; i < 4; i++ {
 24 | 		history.Add("module", "target", fmt.Sprintf("result %d", i), true)
 25 | 	}
 26 | 
 27 | 	savedResults := history.List()
 28 | 	for i := 0; i < len(savedResults); i++ {
 29 | 		if savedResults[i].DebugOutput != fmt.Sprintf("result %d", i+1) {
 30 | 			t.Errorf("History contained the wrong result at index %d", i)
 31 | 		}
 32 | 	}
 33 | }
 34 | 
 35 | func FillHistoryWithMaxSuccesses(h *ResultHistory) {
 36 | 	for i := uint(0); i < h.MaxResults; i++ {
 37 | 		h.Add("module", "target", fmt.Sprintf("result %d", h.nextId), true)
 38 | 	}
 39 | }
 40 | 
 41 | func FillHistoryWithMaxPreservedFailures(h *ResultHistory) {
 42 | 	for i := uint(0); i < h.MaxResults; i++ {
 43 | 		h.Add("module", "target", fmt.Sprintf("result %d", h.nextId), false)
 44 | 	}
 45 | }
 46 | 
 47 | func TestHistoryPreservesExpiredFailedResults(t *testing.T) {
 48 | 	history := &ResultHistory{MaxResults: 3}
 49 | 
 50 | 	// Success are expired, no failures are expired
 51 | 	FillHistoryWithMaxSuccesses(history)
 52 | 	FillHistoryWithMaxPreservedFailures(history)
 53 | 	savedResults := history.List()
 54 | 	for i := uint(0); i < uint(len(savedResults)); i++ {
 55 | 		expectedDebugOutput := fmt.Sprintf("result %d", i+history.MaxResults)
 56 | 		if savedResults[i].DebugOutput != expectedDebugOutput {
 57 | 			t.Errorf("History contained the wrong result at index %d. Expected: %s, Actual: %s", i, expectedDebugOutput, savedResults[i].DebugOutput)
 58 | 		}
 59 | 	}
 60 | 
 61 | 	// Failures are expired, should all be preserved
 62 | 	FillHistoryWithMaxPreservedFailures(history)
 63 | 	savedResults = history.List()
 64 | 	for i := uint(0); i < uint(len(savedResults)); i++ {
 65 | 		expectedDebugOutput := fmt.Sprintf("result %d", i+history.MaxResults)
 66 | 		if savedResults[i].DebugOutput != expectedDebugOutput {
 67 | 			t.Errorf("History contained the wrong result at index %d. Expected: %s, Actual: %s", i, expectedDebugOutput, savedResults[i].DebugOutput)
 68 | 		}
 69 | 	}
 70 | 
 71 | 	// New expired failures are preserved, new success are not expired
 72 | 	FillHistoryWithMaxPreservedFailures(history)
 73 | 	FillHistoryWithMaxSuccesses(history)
 74 | 	savedResults = history.List()
 75 | 	for i := uint(0); i < uint(len(savedResults)); i++ {
 76 | 		expectedDebugOutput := fmt.Sprintf("result %d", i+history.MaxResults*3)
 77 | 		if savedResults[i].DebugOutput != expectedDebugOutput {
 78 | 			t.Errorf("History contained the wrong result at index %d. Expected: %s, Actual: %s", i, expectedDebugOutput, savedResults[i].DebugOutput)
 79 | 		}
 80 | 	}
 81 | }
 82 | 
 83 | func TestHistoryGetById(t *testing.T) {
 84 | 	history := &ResultHistory{MaxResults: 2}
 85 | 
 86 | 	history.Add("module", "target-0", fmt.Sprintf("result %d", history.nextId), true)
 87 | 	history.Add("module", "target-1", fmt.Sprintf("result %d", history.nextId), false)
 88 | 
 89 | 	// Get a Result object for a target that exists
 90 | 	resultTrue := history.GetById(0)
 91 | 	if resultTrue == nil {
 92 | 		t.Errorf("Error finding the result in history by id for id: 1")
 93 | 	} else {
 94 | 		if resultTrue.Id != 0 {
 95 | 			t.Errorf("Error finding the result in history by id: expected \"%d\" and got \"%d\"", 0, resultTrue.Id)
 96 | 		}
 97 | 	}
 98 | 
 99 | 	resultFalse := history.GetById(1)
100 | 	if resultFalse == nil {
101 | 		t.Errorf("Error finding the result in history by id for id: 1")
102 | 	} else {
103 | 		if resultFalse.Id != 1 {
104 | 			t.Errorf("Error finding the result in history by id: expected \"%d\" and got \"%d\"", 1, resultFalse.Id)
105 | 		}
106 | 	}
107 | 
108 | 	// Get a Result object for a target that doesn't exist
109 | 	if history.GetById(5) != nil {
110 | 		t.Errorf("Error finding the result in history by id for id: 5")
111 | 	}
112 | }
113 | 
114 | func TestHistoryGetByTarget(t *testing.T) {
115 | 	history := &ResultHistory{MaxResults: 2}
116 | 
117 | 	history.Add("module", "target-0", fmt.Sprintf("result %d", history.nextId), true)
118 | 	history.Add("module", "target-1", fmt.Sprintf("result %d", history.nextId), false)
119 | 
120 | 	// Get a Result object for a target that exists
121 | 	resultTrue := history.GetByTarget("target-0")
122 | 	if resultTrue == nil {
123 | 		t.Errorf("Error finding the result in history by target for target-0")
124 | 	} else {
125 | 		if resultTrue.Target != "target-0" {
126 | 			t.Errorf("Error finding the result in history by target for target: expected \"%s\" and got \"%s\"", "target-0", resultTrue.Target)
127 | 		}
128 | 	}
129 | 
130 | 	resultFalse := history.GetByTarget("target-1")
131 | 	if resultFalse == nil {
132 | 		t.Errorf("Error finding the result in history by target for target-1")
133 | 	} else {
134 | 		if resultFalse.Target != "target-1" {
135 | 			t.Errorf("Error finding the result in history by target for target: expected \"%s\" and got \"%s\"", "target-1", resultFalse.Target)
136 | 		}
137 | 	}
138 | 
139 | 	// Get a Result object for a target that doesn't exist
140 | 	if history.GetByTarget("target-5") != nil {
141 | 		t.Errorf("Error finding the result in history by target for target-5")
142 | 	}
143 | }
144 | 


--------------------------------------------------------------------------------
/prober/icmp.go:
--------------------------------------------------------------------------------
  1 | // Copyright 2016 The Prometheus Authors
  2 | // Licensed under the Apache License, Version 2.0 (the "License");
  3 | // you may not use this file except in compliance with the License.
  4 | // You may obtain a copy of the License at
  5 | //
  6 | // http://www.apache.org/licenses/LICENSE-2.0
  7 | //
  8 | // Unless required by applicable law or agreed to in writing, software
  9 | // distributed under the License is distributed on an "AS IS" BASIS,
 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 11 | // See the License for the specific language governing permissions and
 12 | // limitations under the License.
 13 | 
 14 | package prober
 15 | 
 16 | import (
 17 | 	"bytes"
 18 | 	"context"
 19 | 	"log/slog"
 20 | 	"math/rand"
 21 | 	"net"
 22 | 	"os"
 23 | 	"runtime"
 24 | 	"sync"
 25 | 	"time"
 26 | 
 27 | 	"github.com/prometheus/client_golang/prometheus"
 28 | 	"golang.org/x/net/icmp"
 29 | 	"golang.org/x/net/ipv4"
 30 | 	"golang.org/x/net/ipv6"
 31 | 
 32 | 	"github.com/prometheus/blackbox_exporter/config"
 33 | )
 34 | 
 35 | var (
 36 | 	icmpID            int
 37 | 	icmpSequence      uint16
 38 | 	icmpSequenceMutex sync.Mutex
 39 | )
 40 | 
 41 | func init() {
 42 | 	r := rand.New(rand.NewSource(time.Now().UnixNano()))
 43 | 	// PID is typically 1 when running in a container; in that case, set
 44 | 	// the ICMP echo ID to a random value to avoid potential clashes with
 45 | 	// other blackbox_exporter instances. See #411.
 46 | 	if pid := os.Getpid(); pid == 1 {
 47 | 		icmpID = r.Intn(1 << 16)
 48 | 	} else {
 49 | 		icmpID = pid & 0xffff
 50 | 	}
 51 | 
 52 | 	// Start the ICMP echo sequence at a random offset to prevent them from
 53 | 	// being in sync when several blackbox_exporter instances are restarted
 54 | 	// at the same time. See #411.
 55 | 	icmpSequence = uint16(r.Intn(1 << 16))
 56 | }
 57 | 
 58 | func getICMPSequence() uint16 {
 59 | 	icmpSequenceMutex.Lock()
 60 | 	defer icmpSequenceMutex.Unlock()
 61 | 	icmpSequence++
 62 | 	return icmpSequence
 63 | }
 64 | 
 65 | func ProbeICMP(ctx context.Context, target string, module config.Module, registry *prometheus.Registry, logger *slog.Logger) (success bool) {
 66 | 	var (
 67 | 		requestType     icmp.Type
 68 | 		replyType       icmp.Type
 69 | 		icmpConn        *icmp.PacketConn
 70 | 		v4RawConn       *ipv4.RawConn
 71 | 		hopLimitFlagSet = true
 72 | 
 73 | 		durationGaugeVec = prometheus.NewGaugeVec(prometheus.GaugeOpts{
 74 | 			Name: "probe_icmp_duration_seconds",
 75 | 			Help: "Duration of icmp request by phase",
 76 | 		}, []string{"phase"})
 77 | 
 78 | 		hopLimitGauge = prometheus.NewGauge(prometheus.GaugeOpts{
 79 | 			Name: "probe_icmp_reply_hop_limit",
 80 | 			Help: "Replied packet hop limit (TTL for ipv4)",
 81 | 		})
 82 | 	)
 83 | 
 84 | 	for _, lv := range []string{"resolve", "setup", "rtt"} {
 85 | 		durationGaugeVec.WithLabelValues(lv)
 86 | 	}
 87 | 
 88 | 	registry.MustRegister(durationGaugeVec)
 89 | 
 90 | 	dstIPAddr, lookupTime, err := chooseProtocol(ctx, module.ICMP.IPProtocol, module.ICMP.IPProtocolFallback, target, registry, logger)
 91 | 
 92 | 	if err != nil {
 93 | 		logger.Error("Error resolving address", "err", err)
 94 | 		return false
 95 | 	}
 96 | 	durationGaugeVec.WithLabelValues("resolve").Add(lookupTime)
 97 | 
 98 | 	var srcIP net.IP
 99 | 	if len(module.ICMP.SourceIPAddress) > 0 {
100 | 		if srcIP = net.ParseIP(module.ICMP.SourceIPAddress); srcIP == nil {
101 | 			logger.Error("Error parsing source ip address", "srcIP", module.ICMP.SourceIPAddress)
102 | 			return false
103 | 		}
104 | 		logger.Info("Using source address", "srcIP", srcIP)
105 | 	}
106 | 
107 | 	setupStart := time.Now()
108 | 	logger.Info("Creating socket")
109 | 
110 | 	privileged := true
111 | 	// Unprivileged sockets are supported on Darwin and Linux only.
112 | 	tryUnprivileged := runtime.GOOS == "darwin" || runtime.GOOS == "linux"
113 | 
114 | 	if dstIPAddr.IP.To4() == nil {
115 | 		requestType = ipv6.ICMPTypeEchoRequest
116 | 		replyType = ipv6.ICMPTypeEchoReply
117 | 
118 | 		if srcIP == nil {
119 | 			srcIP = net.ParseIP("::")
120 | 		}
121 | 
122 | 		if tryUnprivileged {
123 | 			// "udp" here means unprivileged -- not the protocol "udp".
124 | 			icmpConn, err = icmp.ListenPacket("udp6", srcIP.String())
125 | 			if err != nil {
126 | 				logger.Debug("Unable to do unprivileged listen on socket, will attempt privileged", "err", err)
127 | 			} else {
128 | 				privileged = false
129 | 			}
130 | 		}
131 | 
132 | 		if privileged {
133 | 			icmpConn, err = icmp.ListenPacket("ip6:ipv6-icmp", srcIP.String())
134 | 			if err != nil {
135 | 				logger.Error("Error listening to socket", "err", err)
136 | 				return
137 | 			}
138 | 		}
139 | 		defer icmpConn.Close()
140 | 
141 | 		if err := icmpConn.IPv6PacketConn().SetControlMessage(ipv6.FlagHopLimit, true); err != nil {
142 | 			logger.Debug("Failed to set Control Message for retrieving Hop Limit", "err", err)
143 | 			hopLimitFlagSet = false
144 | 		}
145 | 	} else {
146 | 		requestType = ipv4.ICMPTypeEcho
147 | 		replyType = ipv4.ICMPTypeEchoReply
148 | 
149 | 		if srcIP == nil {
150 | 			srcIP = net.ParseIP("0.0.0.0")
151 | 		}
152 | 
153 | 		if module.ICMP.DontFragment {
154 | 			// If the user has set the don't fragment option we cannot use unprivileged
155 | 			// sockets as it is not possible to set IP header level options.
156 | 			netConn, err := net.ListenPacket("ip4:icmp", srcIP.String())
157 | 			if err != nil {
158 | 				logger.Error("Error listening to socket", "err", err)
159 | 				return
160 | 			}
161 | 			defer netConn.Close()
162 | 
163 | 			v4RawConn, err = ipv4.NewRawConn(netConn)
164 | 			if err != nil {
165 | 				logger.Error("Error creating raw connection", "err", err)
166 | 				return
167 | 			}
168 | 			defer v4RawConn.Close()
169 | 
170 | 			if err := v4RawConn.SetControlMessage(ipv4.FlagTTL, true); err != nil {
171 | 				logger.Debug("Failed to set Control Message for retrieving TTL", "err", err)
172 | 				hopLimitFlagSet = false
173 | 			}
174 | 		} else {
175 | 			if tryUnprivileged {
176 | 				icmpConn, err = icmp.ListenPacket("udp4", srcIP.String())
177 | 				if err != nil {
178 | 					logger.Debug("Unable to do unprivileged listen on socket, will attempt privileged", "err", err)
179 | 				} else {
180 | 					privileged = false
181 | 				}
182 | 			}
183 | 
184 | 			if privileged {
185 | 				icmpConn, err = icmp.ListenPacket("ip4:icmp", srcIP.String())
186 | 				if err != nil {
187 | 					logger.Error("Error listening to socket", "err", err)
188 | 					return
189 | 				}
190 | 			}
191 | 			defer icmpConn.Close()
192 | 
193 | 			if err := icmpConn.IPv4PacketConn().SetControlMessage(ipv4.FlagTTL, true); err != nil {
194 | 				logger.Debug("Failed to set Control Message for retrieving TTL", "err", err)
195 | 				hopLimitFlagSet = false
196 | 			}
197 | 		}
198 | 	}
199 | 
200 | 	var dst net.Addr = dstIPAddr
201 | 	if !privileged {
202 | 		dst = &net.UDPAddr{IP: dstIPAddr.IP, Zone: dstIPAddr.Zone}
203 | 	}
204 | 
205 | 	var data []byte
206 | 	if module.ICMP.PayloadSize != 0 {
207 | 		data = make([]byte, module.ICMP.PayloadSize)
208 | 		copy(data, "Prometheus Blackbox Exporter")
209 | 	} else {
210 | 		data = []byte("Prometheus Blackbox Exporter")
211 | 	}
212 | 
213 | 	body := &icmp.Echo{
214 | 		ID:   icmpID,
215 | 		Seq:  int(getICMPSequence()),
216 | 		Data: data,
217 | 	}
218 | 	logger.Info("Creating ICMP packet", "seq", body.Seq, "id", body.ID)
219 | 	wm := icmp.Message{
220 | 		Type: requestType,
221 | 		Code: 0,
222 | 		Body: body,
223 | 	}
224 | 
225 | 	wb, err := wm.Marshal(nil)
226 | 	if err != nil {
227 | 		logger.Error("Error marshalling packet", "err", err)
228 | 		return
229 | 	}
230 | 
231 | 	durationGaugeVec.WithLabelValues("setup").Add(time.Since(setupStart).Seconds())
232 | 	logger.Info("Writing out packet")
233 | 	rttStart := time.Now()
234 | 
235 | 	if icmpConn != nil {
236 | 		ttl := module.ICMP.TTL
237 | 		if ttl > 0 {
238 | 			if c4 := icmpConn.IPv4PacketConn(); c4 != nil {
239 | 				logger.Debug("Setting TTL (IPv4 unprivileged)", "ttl", ttl)
240 | 				c4.SetTTL(ttl)
241 | 			}
242 | 			if c6 := icmpConn.IPv6PacketConn(); c6 != nil {
243 | 				logger.Debug("Setting TTL (IPv6 unprivileged)", "ttl", ttl)
244 | 				c6.SetHopLimit(ttl)
245 | 			}
246 | 		}
247 | 		_, err = icmpConn.WriteTo(wb, dst)
248 | 	} else {
249 | 		ttl := config.DefaultICMPTTL
250 | 		if module.ICMP.TTL > 0 {
251 | 			logger.Debug("Overriding TTL (raw IPv4)", "ttl", ttl)
252 | 			ttl = module.ICMP.TTL
253 | 		}
254 | 		// Only for IPv4 raw. Needed for setting DontFragment flag.
255 | 		header := &ipv4.Header{
256 | 			Version:  ipv4.Version,
257 | 			Len:      ipv4.HeaderLen,
258 | 			Protocol: 1,
259 | 			TotalLen: ipv4.HeaderLen + len(wb),
260 | 			TTL:      ttl,
261 | 			Dst:      dstIPAddr.IP,
262 | 			Src:      srcIP,
263 | 		}
264 | 
265 | 		header.Flags |= ipv4.DontFragment
266 | 
267 | 		err = v4RawConn.WriteTo(header, wb, nil)
268 | 	}
269 | 	if err != nil {
270 | 		logger.Warn("Error writing to socket", "err", err)
271 | 		return
272 | 	}
273 | 
274 | 	// Reply should be the same except for the message type and ID if
275 | 	// unprivileged sockets were used and the kernel used its own.
276 | 	wm.Type = replyType
277 | 	// Unprivileged cannot set IDs on Linux.
278 | 	idUnknown := !privileged && runtime.GOOS == "linux"
279 | 	if idUnknown {
280 | 		body.ID = 0
281 | 	}
282 | 	wb, err = wm.Marshal(nil)
283 | 	if err != nil {
284 | 		logger.Error("Error marshalling packet", "err", err)
285 | 		return
286 | 	}
287 | 
288 | 	if idUnknown {
289 | 		// If the ID is unknown (due to unprivileged sockets) we also cannot know
290 | 		// the checksum in userspace.
291 | 		wb[2] = 0
292 | 		wb[3] = 0
293 | 	}
294 | 
295 | 	rb := make([]byte, 65536)
296 | 	deadline, _ := ctx.Deadline()
297 | 	if icmpConn != nil {
298 | 		err = icmpConn.SetReadDeadline(deadline)
299 | 	} else {
300 | 		err = v4RawConn.SetReadDeadline(deadline)
301 | 	}
302 | 	if err != nil {
303 | 		logger.Error("Error setting socket deadline", "err", err)
304 | 		return
305 | 	}
306 | 	logger.Info("Waiting for reply packets")
307 | 	for {
308 | 		var n int
309 | 		var peer net.Addr
310 | 		var err error
311 | 		var hopLimit float64 = -1
312 | 
313 | 		if dstIPAddr.IP.To4() == nil {
314 | 			var cm *ipv6.ControlMessage
315 | 			n, cm, peer, err = icmpConn.IPv6PacketConn().ReadFrom(rb)
316 | 			// HopLimit == 0 is valid for IPv6, although go initialize it as 0.
317 | 			if cm != nil && hopLimitFlagSet {
318 | 				hopLimit = float64(cm.HopLimit)
319 | 			} else {
320 | 				logger.Debug("Cannot get Hop Limit from the received packet. 'probe_icmp_reply_hop_limit' will be missing.")
321 | 			}
322 | 		} else {
323 | 			var cm *ipv4.ControlMessage
324 | 			if icmpConn != nil {
325 | 				n, cm, peer, err = icmpConn.IPv4PacketConn().ReadFrom(rb)
326 | 			} else {
327 | 				var h *ipv4.Header
328 | 				var p []byte
329 | 				h, p, cm, err = v4RawConn.ReadFrom(rb)
330 | 				if err == nil {
331 | 					copy(rb, p)
332 | 					n = len(p)
333 | 					peer = &net.IPAddr{IP: h.Src}
334 | 				}
335 | 			}
336 | 			if cm != nil && hopLimitFlagSet {
337 | 				// Not really Hop Limit, but it is in practice.
338 | 				hopLimit = float64(cm.TTL)
339 | 			} else {
340 | 				logger.Debug("Cannot get TTL from the received packet. 'probe_icmp_reply_hop_limit' will be missing.")
341 | 			}
342 | 		}
343 | 		if err != nil {
344 | 			if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
345 | 				logger.Warn("Timeout reading from socket", "err", err)
346 | 				return
347 | 			}
348 | 			logger.Error("Error reading from socket", "err", err)
349 | 			continue
350 | 		}
351 | 		if peer.String() != dst.String() {
352 | 			continue
353 | 		}
354 | 		if idUnknown {
355 | 			// Clear the ID from the packet, as the kernel will have replaced it (and
356 | 			// kept track of our packet for us, hence clearing is safe).
357 | 			rb[4] = 0
358 | 			rb[5] = 0
359 | 		}
360 | 		if idUnknown || replyType == ipv6.ICMPTypeEchoReply {
361 | 			// Clear checksum to make comparison succeed.
362 | 			rb[2] = 0
363 | 			rb[3] = 0
364 | 		}
365 | 		if bytes.Equal(rb[:n], wb) {
366 | 			durationGaugeVec.WithLabelValues("rtt").Add(time.Since(rttStart).Seconds())
367 | 			if hopLimit >= 0 {
368 | 				hopLimitGauge.Set(hopLimit)
369 | 				registry.MustRegister(hopLimitGauge)
370 | 			}
371 | 			logger.Info("Found matching reply packet")
372 | 			return true
373 | 		}
374 | 	}
375 | }
376 | 


--------------------------------------------------------------------------------
/prober/prober.go:
--------------------------------------------------------------------------------
 1 | // Copyright 2016 The Prometheus Authors
 2 | // Licensed under the Apache License, Version 2.0 (the "License");
 3 | // you may not use this file except in compliance with the License.
 4 | // You may obtain a copy of the License at
 5 | //
 6 | // http://www.apache.org/licenses/LICENSE-2.0
 7 | //
 8 | // Unless required by applicable law or agreed to in writing, software
 9 | // distributed under the License is distributed on an "AS IS" BASIS,
10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | // See the License for the specific language governing permissions and
12 | // limitations under the License.
13 | 
14 | package prober
15 | 
16 | import (
17 | 	"context"
18 | 	"log/slog"
19 | 
20 | 	"github.com/prometheus/client_golang/prometheus"
21 | 
22 | 	"github.com/prometheus/blackbox_exporter/config"
23 | )
24 | 
25 | type ProbeFn func(ctx context.Context, target string, config config.Module, registry *prometheus.Registry, logger *slog.Logger) bool
26 | 
27 | const (
28 | 	helpSSLEarliestCertExpiry     = "Returns last SSL chain expiry in unixtime"
29 | 	helpSSLChainExpiryInTimeStamp = "Returns last SSL chain expiry in timestamp"
30 | 	helpProbeTLSInfo              = "Returns the TLS version used or NaN when unknown"
31 | 	helpProbeTLSCipher            = "Returns the TLS cipher negotiated during handshake"
32 | )
33 | 
34 | var (
35 | 	sslEarliestCertExpiryGaugeOpts = prometheus.GaugeOpts{
36 | 		Name: "probe_ssl_earliest_cert_expiry",
37 | 		Help: helpSSLEarliestCertExpiry,
38 | 	}
39 | 
40 | 	sslChainExpiryInTimeStampGaugeOpts = prometheus.GaugeOpts{
41 | 		Name: "probe_ssl_last_chain_expiry_timestamp_seconds",
42 | 		Help: helpSSLChainExpiryInTimeStamp,
43 | 	}
44 | 
45 | 	probeTLSInfoGaugeOpts = prometheus.GaugeOpts{
46 | 		Name: "probe_tls_version_info",
47 | 		Help: helpProbeTLSInfo,
48 | 	}
49 | 
50 | 	probeTLSCipherGaugeOpts = prometheus.GaugeOpts{
51 | 		Name: "probe_tls_cipher_info",
52 | 		Help: helpProbeTLSCipher,
53 | 	}
54 | )
55 | 


--------------------------------------------------------------------------------
/prober/tcp.go:
--------------------------------------------------------------------------------
  1 | // Copyright 2016 The Prometheus Authors
  2 | // Licensed under the Apache License, Version 2.0 (the "License");
  3 | // you may not use this file except in compliance with the License.
  4 | // You may obtain a copy of the License at
  5 | //
  6 | // http://www.apache.org/licenses/LICENSE-2.0
  7 | //
  8 | // Unless required by applicable law or agreed to in writing, software
  9 | // distributed under the License is distributed on an "AS IS" BASIS,
 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 11 | // See the License for the specific language governing permissions and
 12 | // limitations under the License.
 13 | 
 14 | package prober
 15 | 
 16 | import (
 17 | 	"bufio"
 18 | 	"context"
 19 | 	"crypto/tls"
 20 | 	"fmt"
 21 | 	"log/slog"
 22 | 	"net"
 23 | 
 24 | 	"github.com/prometheus/client_golang/prometheus"
 25 | 	pconfig "github.com/prometheus/common/config"
 26 | 
 27 | 	"github.com/prometheus/blackbox_exporter/config"
 28 | )
 29 | 
 30 | func dialTCP(ctx context.Context, target string, module config.Module, registry *prometheus.Registry, logger *slog.Logger) (net.Conn, error) {
 31 | 	var dialProtocol, dialTarget string
 32 | 	dialer := &net.Dialer{}
 33 | 	targetAddress, port, err := net.SplitHostPort(target)
 34 | 	if err != nil {
 35 | 		logger.Error("Error splitting target address and port", "err", err)
 36 | 		return nil, err
 37 | 	}
 38 | 
 39 | 	ip, _, err := chooseProtocol(ctx, module.TCP.IPProtocol, module.TCP.IPProtocolFallback, targetAddress, registry, logger)
 40 | 	if err != nil {
 41 | 		logger.Error("Error resolving address", "err", err)
 42 | 		return nil, err
 43 | 	}
 44 | 
 45 | 	if ip.IP.To4() == nil {
 46 | 		dialProtocol = "tcp6"
 47 | 	} else {
 48 | 		dialProtocol = "tcp4"
 49 | 	}
 50 | 
 51 | 	if len(module.TCP.SourceIPAddress) > 0 {
 52 | 		srcIP := net.ParseIP(module.TCP.SourceIPAddress)
 53 | 		if srcIP == nil {
 54 | 			logger.Error("Error parsing source ip address", "srcIP", module.TCP.SourceIPAddress)
 55 | 			return nil, fmt.Errorf("error parsing source ip address: %s", module.TCP.SourceIPAddress)
 56 | 		}
 57 | 		logger.Info("Using local address", "srcIP", srcIP)
 58 | 		dialer.LocalAddr = &net.TCPAddr{IP: srcIP}
 59 | 	}
 60 | 
 61 | 	dialTarget = net.JoinHostPort(ip.String(), port)
 62 | 
 63 | 	if !module.TCP.TLS {
 64 | 		logger.Info("Dialing TCP without TLS")
 65 | 		return dialer.DialContext(ctx, dialProtocol, dialTarget)
 66 | 	}
 67 | 	tlsConfig, err := pconfig.NewTLSConfig(&module.TCP.TLSConfig)
 68 | 	if err != nil {
 69 | 		logger.Error("Error creating TLS configuration", "err", err)
 70 | 		return nil, err
 71 | 	}
 72 | 
 73 | 	if len(tlsConfig.ServerName) == 0 {
 74 | 		// If there is no `server_name` in tls_config, use
 75 | 		// targetAddress as TLS-servername. Normally tls.DialWithDialer
 76 | 		// would do this for us, but we pre-resolved the name by
 77 | 		// `chooseProtocol` and pass the IP-address for dialing (prevents
 78 | 		// resolving twice).
 79 | 		// For this reason we need to specify the original targetAddress
 80 | 		// via tlsConfig to enable hostname verification.
 81 | 		tlsConfig.ServerName = targetAddress
 82 | 	}
 83 | 	timeoutDeadline, _ := ctx.Deadline()
 84 | 	dialer.Deadline = timeoutDeadline
 85 | 
 86 | 	logger.Info("Dialing TCP with TLS")
 87 | 	return tls.DialWithDialer(dialer, dialProtocol, dialTarget, tlsConfig)
 88 | }
 89 | 
 90 | func probeExpectInfo(registry *prometheus.Registry, qr *config.QueryResponse, bytes []byte, match []int) {
 91 | 	var names []string
 92 | 	var values []string
 93 | 	for _, s := range qr.Labels {
 94 | 		names = append(names, s.Name)
 95 | 		values = append(values, string(qr.Expect.Expand(nil, []byte(s.Value), bytes, match)))
 96 | 	}
 97 | 	metric := prometheus.NewGaugeVec(
 98 | 		prometheus.GaugeOpts{
 99 | 			Name: "probe_expect_info",
100 | 			Help: "Explicit content matched",
101 | 		},
102 | 		names,
103 | 	)
104 | 	registry.MustRegister(metric)
105 | 	metric.WithLabelValues(values...).Set(1)
106 | }
107 | 
108 | func ProbeTCP(ctx context.Context, target string, module config.Module, registry *prometheus.Registry, logger *slog.Logger) bool {
109 | 	probeSSLEarliestCertExpiry := prometheus.NewGauge(sslEarliestCertExpiryGaugeOpts)
110 | 	probeSSLLastChainExpiryTimestampSeconds := prometheus.NewGauge(sslChainExpiryInTimeStampGaugeOpts)
111 | 	probeSSLLastInformation := prometheus.NewGaugeVec(
112 | 		prometheus.GaugeOpts{
113 | 			Name: "probe_ssl_last_chain_info",
114 | 			Help: "Contains SSL leaf certificate information",
115 | 		},
116 | 		[]string{"fingerprint_sha256", "subject", "issuer", "subjectalternative", "serialnumber"},
117 | 	)
118 | 	probeTLSVersion := prometheus.NewGaugeVec(
119 | 		probeTLSInfoGaugeOpts,
120 | 		[]string{"version"},
121 | 	)
122 | 	probeFailedDueToRegex := prometheus.NewGauge(prometheus.GaugeOpts{
123 | 		Name: "probe_failed_due_to_regex",
124 | 		Help: "Indicates if probe failed due to regex",
125 | 	})
126 | 	registry.MustRegister(probeFailedDueToRegex)
127 | 	deadline, _ := ctx.Deadline()
128 | 
129 | 	conn, err := dialTCP(ctx, target, module, registry, logger)
130 | 	if err != nil {
131 | 		logger.Error("Error dialing TCP", "err", err)
132 | 		return false
133 | 	}
134 | 	defer conn.Close()
135 | 	logger.Info("Successfully dialed")
136 | 
137 | 	// Set a deadline to prevent the following code from blocking forever.
138 | 	// If a deadline cannot be set, better fail the probe by returning an error
139 | 	// now rather than blocking forever.
140 | 	if err := conn.SetDeadline(deadline); err != nil {
141 | 		logger.Error("Error setting deadline", "err", err)
142 | 		return false
143 | 	}
144 | 	if module.TCP.TLS {
145 | 		state := conn.(*tls.Conn).ConnectionState()
146 | 		registry.MustRegister(probeSSLEarliestCertExpiry, probeTLSVersion, probeSSLLastChainExpiryTimestampSeconds, probeSSLLastInformation)
147 | 		probeSSLEarliestCertExpiry.Set(float64(getEarliestCertExpiry(&state).Unix()))
148 | 		probeTLSVersion.WithLabelValues(getTLSVersion(&state)).Set(1)
149 | 		probeSSLLastChainExpiryTimestampSeconds.Set(float64(getLastChainExpiry(&state).Unix()))
150 | 		probeSSLLastInformation.WithLabelValues(getFingerprint(&state), getSubject(&state), getIssuer(&state), getDNSNames(&state), getSerialNumber(&state)).Set(1)
151 | 	}
152 | 	scanner := bufio.NewScanner(conn)
153 | 	for i, qr := range module.TCP.QueryResponse {
154 | 		logger.Info("Processing query response entry", "entry_number", i)
155 | 		send := qr.Send
156 | 		if qr.Expect.Regexp != nil {
157 | 			var match []int
158 | 			// Read lines until one of them matches the configured regexp.
159 | 			for scanner.Scan() {
160 | 				logger.Debug("Read line", "line", scanner.Text())
161 | 				match = qr.Expect.FindSubmatchIndex(scanner.Bytes())
162 | 				if match != nil {
163 | 					logger.Info("Regexp matched", "regexp", qr.Expect.Regexp, "line", scanner.Text())
164 | 					break
165 | 				}
166 | 			}
167 | 			if scanner.Err() != nil {
168 | 				logger.Error("Error reading from connection", "err", scanner.Err().Error())
169 | 				return false
170 | 			}
171 | 			if match == nil {
172 | 				probeFailedDueToRegex.Set(1)
173 | 				logger.Error("Regexp did not match", "regexp", qr.Expect.Regexp, "line", scanner.Text())
174 | 				return false
175 | 			}
176 | 			probeFailedDueToRegex.Set(0)
177 | 			send = string(qr.Expect.Expand(nil, []byte(send), scanner.Bytes(), match))
178 | 			if qr.Labels != nil {
179 | 				probeExpectInfo(registry, &qr, scanner.Bytes(), match)
180 | 			}
181 | 		}
182 | 		if send != "" {
183 | 			logger.Debug("Sending line", "line", send)
184 | 			if _, err := fmt.Fprintf(conn, "%s\n", send); err != nil {
185 | 				logger.Error("Failed to send", "err", err)
186 | 				return false
187 | 			}
188 | 		}
189 | 		if qr.StartTLS {
190 | 			// Upgrade TCP connection to TLS.
191 | 			tlsConfig, err := pconfig.NewTLSConfig(&module.TCP.TLSConfig)
192 | 			if err != nil {
193 | 				logger.Error("Failed to create TLS configuration", "err", err)
194 | 				return false
195 | 			}
196 | 			if tlsConfig.ServerName == "" {
197 | 				// Use target-hostname as default for TLS-servername.
198 | 				targetAddress, _, _ := net.SplitHostPort(target) // Had succeeded in dialTCP already.
199 | 				tlsConfig.ServerName = targetAddress
200 | 			}
201 | 			tlsConn := tls.Client(conn, tlsConfig)
202 | 			defer tlsConn.Close()
203 | 
204 | 			// Initiate TLS handshake (required here to get TLS state).
205 | 			if err := tlsConn.Handshake(); err != nil {
206 | 				logger.Error("TLS Handshake (client) failed", "err", err)
207 | 				return false
208 | 			}
209 | 			logger.Info("TLS Handshake (client) succeeded.")
210 | 			conn = net.Conn(tlsConn)
211 | 			scanner = bufio.NewScanner(conn)
212 | 
213 | 			// Get certificate expiry.
214 | 			state := tlsConn.ConnectionState()
215 | 			registry.MustRegister(probeSSLEarliestCertExpiry, probeTLSVersion, probeSSLLastChainExpiryTimestampSeconds, probeSSLLastInformation)
216 | 			probeSSLEarliestCertExpiry.Set(float64(getEarliestCertExpiry(&state).Unix()))
217 | 			probeTLSVersion.WithLabelValues(getTLSVersion(&state)).Set(1)
218 | 			probeSSLLastChainExpiryTimestampSeconds.Set(float64(getLastChainExpiry(&state).Unix()))
219 | 			probeSSLLastInformation.WithLabelValues(getFingerprint(&state), getSubject(&state), getIssuer(&state), getDNSNames(&state), getSerialNumber(&state)).Set(1)
220 | 		}
221 | 	}
222 | 	return true
223 | }
224 | 


--------------------------------------------------------------------------------
/prober/tls.go:
--------------------------------------------------------------------------------
 1 | // Copyright 2016 The Prometheus Authors
 2 | // Licensed under the Apache License, Version 2.0 (the "License");
 3 | // you may not use this file except in compliance with the License.
 4 | // You may obtain a copy of the License at
 5 | //
 6 | // http://www.apache.org/licenses/LICENSE-2.0
 7 | //
 8 | // Unless required by applicable law or agreed to in writing, software
 9 | // distributed under the License is distributed on an "AS IS" BASIS,
10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | // See the License for the specific language governing permissions and
12 | // limitations under the License.
13 | 
14 | package prober
15 | 
16 | import (
17 | 	"crypto/sha256"
18 | 	"crypto/tls"
19 | 	"encoding/hex"
20 | 	"fmt"
21 | 	"strings"
22 | 	"time"
23 | )
24 | 
25 | func getEarliestCertExpiry(state *tls.ConnectionState) time.Time {
26 | 	earliest := time.Time{}
27 | 	for _, cert := range state.PeerCertificates {
28 | 		if (earliest.IsZero() || cert.NotAfter.Before(earliest)) && !cert.NotAfter.IsZero() {
29 | 			earliest = cert.NotAfter
30 | 		}
31 | 	}
32 | 	return earliest
33 | }
34 | 
35 | func getFingerprint(state *tls.ConnectionState) string {
36 | 	cert := state.PeerCertificates[0]
37 | 	fingerprint := sha256.Sum256(cert.Raw)
38 | 	return hex.EncodeToString(fingerprint[:])
39 | }
40 | 
41 | func getSubject(state *tls.ConnectionState) string {
42 | 	cert := state.PeerCertificates[0]
43 | 	return cert.Subject.String()
44 | }
45 | 
46 | func getIssuer(state *tls.ConnectionState) string {
47 | 	cert := state.PeerCertificates[0]
48 | 	return cert.Issuer.String()
49 | }
50 | 
51 | func getDNSNames(state *tls.ConnectionState) string {
52 | 	cert := state.PeerCertificates[0]
53 | 	return strings.Join(cert.DNSNames, ",")
54 | }
55 | 
56 | func getLastChainExpiry(state *tls.ConnectionState) time.Time {
57 | 	lastChainExpiry := time.Time{}
58 | 	for _, chain := range state.VerifiedChains {
59 | 		earliestCertExpiry := time.Time{}
60 | 		for _, cert := range chain {
61 | 			if (earliestCertExpiry.IsZero() || cert.NotAfter.Before(earliestCertExpiry)) && !cert.NotAfter.IsZero() {
62 | 				earliestCertExpiry = cert.NotAfter
63 | 			}
64 | 		}
65 | 		if lastChainExpiry.IsZero() || lastChainExpiry.Before(earliestCertExpiry) {
66 | 			lastChainExpiry = earliestCertExpiry
67 | 		}
68 | 
69 | 	}
70 | 	return lastChainExpiry
71 | }
72 | 
73 | func getSerialNumber(state *tls.ConnectionState) string {
74 | 	cert := state.PeerCertificates[0]
75 | 	// Using `cert.SerialNumber.Text(16)` will drop the leading zeros when converting the SerialNumber to String, see https://github.com/mozilla/tls-observatory/pull/245.
76 | 	// To avoid that, we format in lowercase the bytes with `%x` to base 16, with lower-case letters for a-f, see https://go.dev/play/p/Fylce70N2Zl.
77 | 
78 | 	return fmt.Sprintf("%x", cert.SerialNumber.Bytes())
79 | }
80 | 
81 | func getTLSVersion(state *tls.ConnectionState) string {
82 | 	switch state.Version {
83 | 	case tls.VersionTLS10:
84 | 		return "TLS 1.0"
85 | 	case tls.VersionTLS11:
86 | 		return "TLS 1.1"
87 | 	case tls.VersionTLS12:
88 | 		return "TLS 1.2"
89 | 	case tls.VersionTLS13:
90 | 		return "TLS 1.3"
91 | 	default:
92 | 		return "unknown"
93 | 	}
94 | }
95 | 
96 | func getTLSCipher(state *tls.ConnectionState) string {
97 | 	return tls.CipherSuiteName(state.CipherSuite)
98 | }
99 | 


--------------------------------------------------------------------------------
/prober/utils.go:
--------------------------------------------------------------------------------
  1 | // Copyright 2016 The Prometheus Authors
  2 | // Licensed under the Apache License, Version 2.0 (the "License");
  3 | // you may not use this file except in compliance with the License.
  4 | // You may obtain a copy of the License at
  5 | //
  6 | // http://www.apache.org/licenses/LICENSE-2.0
  7 | //
  8 | // Unless required by applicable law or agreed to in writing, software
  9 | // distributed under the License is distributed on an "AS IS" BASIS,
 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 11 | // See the License for the specific language governing permissions and
 12 | // limitations under the License.
 13 | 
 14 | package prober
 15 | 
 16 | import (
 17 | 	"context"
 18 | 	"fmt"
 19 | 	"hash/fnv"
 20 | 	"log/slog"
 21 | 	"net"
 22 | 	"time"
 23 | 
 24 | 	"github.com/prometheus/client_golang/prometheus"
 25 | )
 26 | 
 27 | var protocolToGauge = map[string]float64{
 28 | 	"ip4": 4,
 29 | 	"ip6": 6,
 30 | }
 31 | 
 32 | // Returns the IP for the IPProtocol and lookup time.
 33 | func chooseProtocol(ctx context.Context, IPProtocol string, fallbackIPProtocol bool, target string, registry *prometheus.Registry, logger *slog.Logger) (ip *net.IPAddr, lookupTime float64, err error) {
 34 | 	var fallbackProtocol string
 35 | 	probeDNSLookupTimeSeconds := prometheus.NewGauge(prometheus.GaugeOpts{
 36 | 		Name: "probe_dns_lookup_time_seconds",
 37 | 		Help: "Returns the time taken for probe dns lookup in seconds",
 38 | 	})
 39 | 
 40 | 	probeIPProtocolGauge := prometheus.NewGauge(prometheus.GaugeOpts{
 41 | 		Name: "probe_ip_protocol",
 42 | 		Help: "Specifies whether probe ip protocol is IP4 or IP6",
 43 | 	})
 44 | 
 45 | 	probeIPAddrHash := prometheus.NewGauge(prometheus.GaugeOpts{
 46 | 		Name: "probe_ip_addr_hash",
 47 | 		Help: "Specifies the hash of IP address. It's useful to detect if the IP address changes.",
 48 | 	})
 49 | 	registry.MustRegister(probeIPProtocolGauge)
 50 | 	registry.MustRegister(probeDNSLookupTimeSeconds)
 51 | 	registry.MustRegister(probeIPAddrHash)
 52 | 
 53 | 	if IPProtocol == "ip6" || IPProtocol == "" {
 54 | 		IPProtocol = "ip6"
 55 | 		fallbackProtocol = "ip4"
 56 | 	} else {
 57 | 		IPProtocol = "ip4"
 58 | 		fallbackProtocol = "ip6"
 59 | 	}
 60 | 
 61 | 	logger.Info("Resolving target address", "target", target, "ip_protocol", IPProtocol)
 62 | 	resolveStart := time.Now()
 63 | 
 64 | 	defer func() {
 65 | 		lookupTime = time.Since(resolveStart).Seconds()
 66 | 		probeDNSLookupTimeSeconds.Add(lookupTime)
 67 | 	}()
 68 | 
 69 | 	resolver := &net.Resolver{}
 70 | 	if !fallbackIPProtocol {
 71 | 		ips, err := resolver.LookupIP(ctx, IPProtocol, target)
 72 | 		if err == nil {
 73 | 			for _, ip := range ips {
 74 | 				logger.Info("Resolved target address", "target", target, "ip", ip.String())
 75 | 				probeIPProtocolGauge.Set(protocolToGauge[IPProtocol])
 76 | 				probeIPAddrHash.Set(ipHash(ip))
 77 | 				return &net.IPAddr{IP: ip}, lookupTime, nil
 78 | 			}
 79 | 		}
 80 | 		logger.Error("Resolution with IP protocol failed", "target", target, "ip_protocol", IPProtocol, "err", err)
 81 | 		return nil, 0.0, err
 82 | 	}
 83 | 
 84 | 	ips, err := resolver.LookupIPAddr(ctx, target)
 85 | 	if err != nil {
 86 | 		logger.Error("Resolution with IP protocol failed", "target", target, "err", err)
 87 | 		return nil, 0.0, err
 88 | 	}
 89 | 
 90 | 	// Return the IP in the requested protocol.
 91 | 	var fallback *net.IPAddr
 92 | 	for _, ip := range ips {
 93 | 		switch IPProtocol {
 94 | 		case "ip4":
 95 | 			if ip.IP.To4() != nil {
 96 | 				logger.Info("Resolved target address", "target", target, "ip", ip.String())
 97 | 				probeIPProtocolGauge.Set(4)
 98 | 				probeIPAddrHash.Set(ipHash(ip.IP))
 99 | 				return &ip, lookupTime, nil
100 | 			}
101 | 
102 | 			// ip4 as fallback
103 | 			fallback = &ip
104 | 
105 | 		case "ip6":
106 | 			if ip.IP.To4() == nil {
107 | 				logger.Info("Resolved target address", "target", target, "ip", ip.String())
108 | 				probeIPProtocolGauge.Set(6)
109 | 				probeIPAddrHash.Set(ipHash(ip.IP))
110 | 				return &ip, lookupTime, nil
111 | 			}
112 | 
113 | 			// ip6 as fallback
114 | 			fallback = &ip
115 | 		}
116 | 	}
117 | 
118 | 	// Unable to find ip and no fallback set.
119 | 	if fallback == nil || !fallbackIPProtocol {
120 | 		return nil, 0.0, fmt.Errorf("unable to find ip; no fallback")
121 | 	}
122 | 
123 | 	// Use fallback ip protocol.
124 | 	if fallbackProtocol == "ip4" {
125 | 		probeIPProtocolGauge.Set(4)
126 | 	} else {
127 | 		probeIPProtocolGauge.Set(6)
128 | 	}
129 | 	probeIPAddrHash.Set(ipHash(fallback.IP))
130 | 	logger.Info("Resolved target address", "target", target, "ip", fallback.String())
131 | 	return fallback, lookupTime, nil
132 | }
133 | 
134 | func ipHash(ip net.IP) float64 {
135 | 	h := fnv.New32a()
136 | 	if ip.To4() != nil {
137 | 		h.Write(ip.To4())
138 | 	} else {
139 | 		h.Write(ip.To16())
140 | 	}
141 | 	return float64(h.Sum32())
142 | }
143 | 


--------------------------------------------------------------------------------
/prober/utils_test.go:
--------------------------------------------------------------------------------
  1 | // Copyright 2016 The Prometheus Authors
  2 | // Licensed under the Apache License, Version 2.0 (the "License");
  3 | // you may not use this file except in compliance with the License.
  4 | // You may obtain a copy of the License at
  5 | //
  6 | // http://www.apache.org/licenses/LICENSE-2.0
  7 | //
  8 | // Unless required by applicable law or agreed to in writing, software
  9 | // distributed under the License is distributed on an "AS IS" BASIS,
 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 11 | // See the License for the specific language governing permissions and
 12 | // limitations under the License.
 13 | 
 14 | package prober
 15 | 
 16 | import (
 17 | 	"context"
 18 | 	"crypto/rand"
 19 | 	"crypto/rsa"
 20 | 	"crypto/tls"
 21 | 	"crypto/x509"
 22 | 	"crypto/x509/pkix"
 23 | 	"encoding/pem"
 24 | 	"fmt"
 25 | 	"math/big"
 26 | 	"net"
 27 | 	"slices"
 28 | 	"testing"
 29 | 	"time"
 30 | 
 31 | 	"github.com/prometheus/client_golang/prometheus"
 32 | 	dto "github.com/prometheus/client_model/go"
 33 | 	"github.com/prometheus/common/promslog"
 34 | )
 35 | 
 36 | // Check if expected results are in the registry
 37 | func checkRegistryResults(expRes map[string]float64, mfs []*dto.MetricFamily, t *testing.T) {
 38 | 	res := make(map[string]float64)
 39 | 	for i := range mfs {
 40 | 		res[mfs[i].GetName()] = mfs[i].Metric[0].GetGauge().GetValue()
 41 | 	}
 42 | 	for k, v := range expRes {
 43 | 		val, ok := res[k]
 44 | 		if !ok {
 45 | 			t.Fatalf("Expected metric %v not found in returned metrics", k)
 46 | 		}
 47 | 		if val != v {
 48 | 			t.Fatalf("Expected: %v: %v, got: %v: %v", k, v, k, val)
 49 | 		}
 50 | 	}
 51 | }
 52 | 
 53 | // Check if expected labels are in the registry
 54 | func checkRegistryLabels(expRes map[string]map[string]string, mfs []*dto.MetricFamily, t *testing.T) {
 55 | 	results := make(map[string]map[string]string)
 56 | 	for _, mf := range mfs {
 57 | 		result := make(map[string]string)
 58 | 		for _, metric := range mf.Metric {
 59 | 			for _, l := range metric.GetLabel() {
 60 | 				result[l.GetName()] = l.GetValue()
 61 | 			}
 62 | 		}
 63 | 		results[mf.GetName()] = result
 64 | 	}
 65 | 
 66 | 	for metric, labelValues := range expRes {
 67 | 		if _, ok := results[metric]; !ok {
 68 | 			t.Fatalf("Expected metric %v not found in returned metrics", metric)
 69 | 		}
 70 | 		for name, exp := range labelValues {
 71 | 			val, ok := results[metric][name]
 72 | 			if !ok {
 73 | 				t.Fatalf("Expected label %v for metric %v not found in returned metrics", val, name)
 74 | 			}
 75 | 			if val != exp {
 76 | 				t.Fatalf("Expected: %v{%q=%q}, got: %v{%q=%q}", metric, name, exp, metric, name, val)
 77 | 			}
 78 | 		}
 79 | 	}
 80 | }
 81 | 
 82 | func generateCertificateTemplate(expiry time.Time, IPAddressSAN bool) *x509.Certificate {
 83 | 	template := &x509.Certificate{
 84 | 		BasicConstraintsValid: true,
 85 | 		SubjectKeyId:          []byte{1},
 86 | 		SerialNumber:          big.NewInt(1),
 87 | 		Subject: pkix.Name{
 88 | 			CommonName:   "Example",
 89 | 			Organization: []string{"Example Org"},
 90 | 		},
 91 | 		NotBefore:   time.Now(),
 92 | 		NotAfter:    expiry,
 93 | 		ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
 94 | 		KeyUsage:    x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
 95 | 	}
 96 | 
 97 | 	template.DNSNames = append(template.DNSNames, "localhost")
 98 | 	if IPAddressSAN {
 99 | 		template.IPAddresses = append(template.IPAddresses, net.ParseIP("127.0.0.1"))
100 | 		template.IPAddresses = append(template.IPAddresses, net.ParseIP("::1"))
101 | 	}
102 | 
103 | 	return template
104 | }
105 | 
106 | func generateCertificate(template, parent *x509.Certificate, publickey *rsa.PublicKey, privatekey *rsa.PrivateKey) (*x509.Certificate, []byte) {
107 | 	derCert, err := x509.CreateCertificate(rand.Reader, template, template, publickey, privatekey)
108 | 	if err != nil {
109 | 		panic(fmt.Sprintf("Error signing test-certificate: %s", err))
110 | 	}
111 | 	cert, err := x509.ParseCertificate(derCert)
112 | 	if err != nil {
113 | 		panic(fmt.Sprintf("Error parsing test-certificate: %s", err))
114 | 	}
115 | 	pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derCert})
116 | 	return cert, pemCert
117 | 
118 | }
119 | 
120 | func generateSignedCertificate(template, parentCert *x509.Certificate, parentKey *rsa.PrivateKey) (*x509.Certificate, []byte, *rsa.PrivateKey) {
121 | 	privatekey, err := rsa.GenerateKey(rand.Reader, 2048)
122 | 	if err != nil {
123 | 		panic(fmt.Sprintf("Error creating rsa key: %s", err))
124 | 	}
125 | 	cert, pemCert := generateCertificate(template, parentCert, &privatekey.PublicKey, parentKey)
126 | 	return cert, pemCert, privatekey
127 | }
128 | 
129 | func generateSelfSignedCertificate(template *x509.Certificate) (*x509.Certificate, []byte, *rsa.PrivateKey) {
130 | 	privatekey, err := rsa.GenerateKey(rand.Reader, 2048)
131 | 	if err != nil {
132 | 		panic(fmt.Sprintf("Error creating rsa key: %s", err))
133 | 	}
134 | 	publickey := &privatekey.PublicKey
135 | 
136 | 	cert, pemCert := generateCertificate(template, template, publickey, privatekey)
137 | 	return cert, pemCert, privatekey
138 | }
139 | 
140 | func generateSelfSignedCertificateWithPrivateKey(template *x509.Certificate, privatekey *rsa.PrivateKey) (*x509.Certificate, []byte) {
141 | 	publickey := &privatekey.PublicKey
142 | 	cert, pemCert := generateCertificate(template, template, publickey, privatekey)
143 | 	return cert, pemCert
144 | }
145 | 
146 | func TestChooseProtocol(t *testing.T) {
147 | 	if testing.Short() {
148 | 		t.Skip("skipping network dependent test")
149 | 	}
150 | 	ctx := context.Background()
151 | 	registry := prometheus.NewPedanticRegistry()
152 | 	logger := promslog.New(&promslog.Config{})
153 | 
154 | 	ip, _, err := chooseProtocol(ctx, "ip4", true, "ipv6.google.com", registry, logger)
155 | 	if err != nil {
156 | 		t.Error(err)
157 | 	}
158 | 	if ip == nil || ip.IP.To4() != nil {
159 | 		t.Error("with fallback it should answer")
160 | 	}
161 | 
162 | 	registry = prometheus.NewPedanticRegistry()
163 | 
164 | 	ip, _, err = chooseProtocol(ctx, "ip4", false, "ipv6.google.com", registry, logger)
165 | 	if err != nil && !err.(*net.DNSError).IsNotFound {
166 | 		t.Error(err)
167 | 	} else if err == nil {
168 | 		t.Error("should set error")
169 | 	}
170 | 	if ip != nil {
171 | 		t.Error("without fallback it should not answer")
172 | 	}
173 | }
174 | 
175 | func checkMetrics(expected map[string]map[string]map[string]struct{}, mfs []*dto.MetricFamily, t *testing.T) {
176 | 	type (
177 | 		valueValidation struct {
178 | 			found bool
179 | 		}
180 | 		labelValidation struct {
181 | 			found  bool
182 | 			values map[string]valueValidation
183 | 		}
184 | 		metricValidation struct {
185 | 			found  bool
186 | 			labels map[string]labelValidation
187 | 		}
188 | 	)
189 | 
190 | 	foundMetrics := map[string]metricValidation{}
191 | 
192 | 	for mname, labels := range expected {
193 | 		var mv metricValidation
194 | 		if labels != nil {
195 | 			mv.labels = map[string]labelValidation{}
196 | 			for lname, values := range labels {
197 | 				var lv labelValidation
198 | 				if values != nil {
199 | 					lv.values = map[string]valueValidation{}
200 | 					for vname := range values {
201 | 						lv.values[vname] = valueValidation{}
202 | 					}
203 | 				}
204 | 				mv.labels[lname] = lv
205 | 			}
206 | 		}
207 | 		foundMetrics[mname] = mv
208 | 	}
209 | 
210 | 	for _, mf := range mfs {
211 | 		info, wanted := foundMetrics[mf.GetName()]
212 | 		if !wanted {
213 | 			continue
214 | 		}
215 | 		info.found = true
216 | 		for _, metric := range mf.GetMetric() {
217 | 			if info.labels == nil {
218 | 				continue
219 | 			}
220 | 			for _, lp := range metric.Label {
221 | 				if label, labelWanted := info.labels[lp.GetName()]; labelWanted {
222 | 					label.found = true
223 | 					if label.values != nil {
224 | 						if value, wanted := label.values[lp.GetValue()]; !wanted {
225 | 							t.Fatalf("Unexpected label %s=%s", lp.GetName(), lp.GetValue())
226 | 						} else if value.found {
227 | 							t.Fatalf("Label %s=%s duplicated", lp.GetName(), lp.GetValue())
228 | 						}
229 | 						label.values[lp.GetValue()] = valueValidation{found: true}
230 | 					}
231 | 					info.labels[lp.GetName()] = label
232 | 				}
233 | 			}
234 | 		}
235 | 		foundMetrics[mf.GetName()] = info
236 | 	}
237 | 
238 | 	for mname, m := range foundMetrics {
239 | 		if !m.found {
240 | 			t.Fatalf("metric %s wanted, not found", mname)
241 | 		}
242 | 		for lname, label := range m.labels {
243 | 			if !label.found {
244 | 				t.Fatalf("metric %s, label %s wanted, not found", mname, lname)
245 | 			}
246 | 			for vname, value := range label.values {
247 | 				if !value.found {
248 | 					t.Fatalf("metric %s, label %s, value %s wanted, not found", mname, lname, vname)
249 | 				}
250 | 			}
251 | 		}
252 | 	}
253 | }
254 | 
255 | func TestGetSerialNumber(t *testing.T) {
256 | 	tests := []struct {
257 | 		name         string
258 | 		serialNumber *big.Int
259 | 		expected     string
260 | 	}{
261 | 		{
262 | 			name: "Serial number with leading zeros",
263 | 			serialNumber: func() *big.Int {
264 | 				serialNumber, _ := new(big.Int).SetString("0BFFBC11F1907D02AF719AFCD64FB253", 16)
265 | 				return serialNumber
266 | 			}(),
267 | 			expected: "0bffbc11f1907d02af719afcd64fb253",
268 | 		},
269 | 		{
270 | 			name: "Serial number without leading zeros",
271 | 			serialNumber: func() *big.Int {
272 | 				serialNumber, _ := new(big.Int).SetString("BBFFBC11F1907D02AF719AFCD64FB253", 16)
273 | 				return serialNumber
274 | 			}(),
275 | 			expected: "bbffbc11f1907d02af719afcd64fb253",
276 | 		},
277 | 	}
278 | 
279 | 	for _, tt := range tests {
280 | 		t.Run(tt.name, func(t *testing.T) {
281 | 			cert := &x509.Certificate{
282 | 				SerialNumber: tt.serialNumber,
283 | 			}
284 | 			state := &tls.ConnectionState{
285 | 				PeerCertificates: []*x509.Certificate{cert},
286 | 			}
287 | 			result := getSerialNumber(state)
288 | 			if result != tt.expected {
289 | 				t.Errorf("expected %s, got %s", tt.expected, result)
290 | 			}
291 | 		})
292 | 	}
293 | }
294 | 
295 | func checkAbsentMetrics(absent []string, mfs []*dto.MetricFamily, t *testing.T) {
296 | 	for _, v := range mfs {
297 | 		name := v.GetName()
298 | 		if slices.Contains(absent, name) {
299 | 			t.Fatalf("metric %s was found but should be absent", name)
300 | 		}
301 | 	}
302 | }
303 | 


--------------------------------------------------------------------------------