Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ modules:
### `<module>`
```yml

# The protocol over which the probe will take place (http, tcp, dns, icmp, grpc).
# The protocol over which the probe will take place (http, tcp, dns, icmp, grpc, unix).
prober: <prober_string>

# How long the probe will wait before giving up.
Expand All @@ -41,6 +41,7 @@ modules:
[ dns: <dns_probe> ]
[ icmp: <icmp_probe> ]
[ grpc: <grpc_probe> ]
[ unix: <unix_probe> ]

```

Expand Down Expand Up @@ -221,6 +222,37 @@ tls_config:

```

### `<unix_probe>`

```yml

# The query sent in the unix socket probe and the expected associated response.
Comment thread
edevil marked this conversation as resolved.
# "expect" matches a regular expression;
# "labels" can define labels which will be exported on metric "probe_expect_info";
# "send" sends some content;
# "send" and "labels.value" can contain values matched by "expect" (such as "${1}");
# "starttls" upgrades connection to TLS.
query_response:
[ - [ [ expect: <string> ],
[ labels:
- [ name: <string>
value: <string>
], ...
],
[ send: <string> ],
[ starttls: <boolean | default = false> ]
], ...
]

# Whether or not TLS is used when the connection is initiated.
[ tls: <boolean | default = false> ]

# Configuration for TLS protocol of unix socket probe.
tls_config:
[ <tls_config> ]

```

### `<dns_probe>`

```yml
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,9 @@ The ICMP probe requires elevated privileges to function:
* *BSD*: root user is required.
* *OS X*: No additional privileges are needed.

The UNIX probe requires the process owner to have write permissions (w) to the UNIX socket,
and access permissions (x) to the directory structure the socket resides in.

[circleci]: https://circleci.com/gh/prometheus/blackbox_exporter
[hub]: https://hub.docker.com/r/prom/blackbox-exporter/
[quay]: https://quay.io/repository/prometheus/blackbox-exporter
21 changes: 21 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ var (
TCP: DefaultTCPProbe,
ICMP: DefaultICMPProbe,
DNS: DefaultDNSProbe,
Unix: DefaultUnixProbe,
}

// DefaultHTTPProbe set default value for HTTPProbe
Expand Down Expand Up @@ -76,6 +77,9 @@ var (
IPProtocolFallback: true,
Recursion: true,
}

// DefaultUnixProbe set default value for UnixProbe
DefaultUnixProbe = UnixProbe{}
)

type Config struct {
Expand Down Expand Up @@ -288,6 +292,7 @@ type Module struct {
ICMP ICMPProbe `yaml:"icmp,omitempty"`
DNS DNSProbe `yaml:"dns,omitempty"`
GRPC GRPCProbe `yaml:"grpc,omitempty"`
Unix UnixProbe `yaml:"unix,omitempty"`
}

type HTTPProbe struct {
Expand Down Expand Up @@ -351,6 +356,12 @@ type TCPProbe struct {
TLSConfig config.TLSConfig `yaml:"tls_config,omitempty"`
}

type UnixProbe struct {
QueryResponse []QueryResponse `yaml:"query_response,omitempty"`
TLS bool `yaml:"tls,omitempty"`
TLSConfig config.TLSConfig `yaml:"tls_config,omitempty"`
}

type ICMPProbe struct {
IPProtocol string `yaml:"preferred_ip_protocol,omitempty"` // Defaults to "ip6".
IPProtocolFallback bool `yaml:"ip_protocol_fallback,omitempty"`
Expand Down Expand Up @@ -511,6 +522,16 @@ func (s *TCPProbe) UnmarshalYAML(unmarshal func(interface{}) error) error {
return nil
}

// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (s *UnixProbe) UnmarshalYAML(unmarshal func(interface{}) error) error {
*s = DefaultUnixProbe
type plain UnixProbe
if err := unmarshal((*plain)(s)); err != nil {
return err
}
return nil
}

// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (s *DNSRRValidator) UnmarshalYAML(unmarshal func(interface{}) error) error {
type plain DNSRRValidator
Expand Down
4 changes: 4 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ func TestLoadBadConfigs(t *testing.T) {
input: "testdata/invalid-tcp-query-response-regexp.yml",
want: `error parsing config file: "Could not compile regular expression" regexp=":["`,
},
{
input: "testdata/invalid-unix-query-response-regexp.yml",
want: `error parsing config file: "Could not compile regular expression" regexp=":["`,
},
{
input: "testdata/invalid-http-body-config.yml",
want: `error parsing config file: setting body and body_file both are not allowed`,
Expand Down
8 changes: 8 additions & 0 deletions config/testdata/invalid-unix-query-response-regexp.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
modules:
unix_socket_bad_regex:
prober: unix
timeout: 5s
unix:
query_response:
- send: "PING"
expect: ":["
7 changes: 7 additions & 0 deletions example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,10 @@ modules:
transport_protocol: "tcp" # defaults to "udp"
preferred_ip_protocol: "ip4" # defaults to "ip6"
query_name: "www.prometheus.io"
unix_socket_ping:
prober: unix
timeout: 5s
unix:
query_response:
- send: "PING"
- expect: "PONG"
1 change: 1 addition & 0 deletions prober/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ var (
"icmp": ProbeICMP,
"dns": ProbeDNS,
"grpc": ProbeGRPC,
"unix": ProbeUnix,
}
)

Expand Down
170 changes: 170 additions & 0 deletions prober/query_response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package prober

import (
"bufio"
"context"
"crypto/tls"
"fmt"
"log/slog"
"net"

"github.com/prometheus/client_golang/prometheus"
pconfig "github.com/prometheus/common/config"

"github.com/prometheus/blackbox_exporter/config"
)

func probeExpectInfo(registry *prometheus.Registry, qr *config.QueryResponse, bytes []byte, match []int) {
var names []string
var values []string
for _, s := range qr.Labels {
names = append(names, s.Name)
values = append(values, string(qr.Expect.Expand(nil, []byte(s.Value), bytes, match)))
}
metric := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "probe_expect_info",
Help: "Explicit content matched",
},
names,
)
registry.MustRegister(metric)
metric.WithLabelValues(values...).Set(1)
}

func probeQueryResponses(ctx context.Context, target string, conn net.Conn, module config.Module, proberName string, registry *prometheus.Registry, logger *slog.Logger) bool {
probeSSLEarliestCertExpiry := prometheus.NewGauge(sslEarliestCertExpiryGaugeOpts)
probeSSLLastChainExpiryTimestampSeconds := prometheus.NewGauge(sslChainExpiryInTimeStampGaugeOpts)
probeSSLLastInformation := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "probe_ssl_last_chain_info",
Help: "Contains SSL leaf certificate information",
},
[]string{"fingerprint_sha256", "subject", "issuer", "subjectalternative", "serialnumber"},
)
probeTLSVersion := prometheus.NewGaugeVec(
probeTLSInfoGaugeOpts,
[]string{"version"},
)
probeFailedDueToRegex := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "probe_failed_due_to_regex",
Help: "Indicates if probe failed due to regex",
})
registry.MustRegister(probeFailedDueToRegex)

var queryResponses []config.QueryResponse
var tlsConfig *pconfig.TLSConfig
var useTLS bool

switch proberName {
case "tcp":
queryResponses = module.TCP.QueryResponse
tlsConfig = &module.TCP.TLSConfig
useTLS = module.TCP.TLS
case "unix":
queryResponses = module.Unix.QueryResponse
tlsConfig = &module.Unix.TLSConfig
useTLS = module.Unix.TLS
}

deadline, _ := ctx.Deadline()
if err := conn.SetDeadline(deadline); err != nil {
logger.Error("Error setting deadline", "err", err)
return false
}

if useTLS {
state := conn.(*tls.Conn).ConnectionState()
registry.MustRegister(probeSSLEarliestCertExpiry, probeTLSVersion, probeSSLLastChainExpiryTimestampSeconds, probeSSLLastInformation)
probeSSLEarliestCertExpiry.Set(float64(getEarliestCertExpiry(&state).Unix()))
probeTLSVersion.WithLabelValues(getTLSVersion(&state)).Set(1)
probeSSLLastChainExpiryTimestampSeconds.Set(float64(getLastChainExpiry(&state).Unix()))
probeSSLLastInformation.WithLabelValues(getFingerprint(&state), getSubject(&state), getIssuer(&state), getDNSNames(&state), getSerialNumber(&state)).Set(1)
}

scanner := bufio.NewScanner(conn)
for i, qr := range queryResponses {
logger.Debug("Processing query response entry", "entry_number", i)
send := qr.Send
if qr.Expect.Regexp != nil {
var match []int
// Read lines until one of them matches the configured regexp.
for scanner.Scan() {
logger.Debug("Read line", "line", scanner.Text())
match = qr.Expect.FindSubmatchIndex(scanner.Bytes())
if match != nil {
logger.Debug("Regexp matched", "regexp", qr.Expect.Regexp, "line", scanner.Text())
break
}
}
if scanner.Err() != nil {
logger.Error("Error reading from connection", "err", scanner.Err().Error())
return false
}
if match == nil {
probeFailedDueToRegex.Set(1)
logger.Error("Regexp did not match", "regexp", qr.Expect.Regexp, "line", scanner.Text())
return false
}
probeFailedDueToRegex.Set(0)
send = string(qr.Expect.Expand(nil, []byte(send), scanner.Bytes(), match))
if qr.Labels != nil {
probeExpectInfo(registry, &qr, scanner.Bytes(), match)
}
}
if send != "" {
logger.Debug("Sending line", "line", send)
if _, err := fmt.Fprintf(conn, "%s\n", send); err != nil {
logger.Error("Failed to send", "err", err)
return false
}
}
if qr.StartTLS {
// Upgrade TCP connection to TLS.
tlsUpgradeConfig, err := pconfig.NewTLSConfig(tlsConfig)
if err != nil {
logger.Error("Failed to create TLS configuration", "err", err)
return false
}
if proberName == "tcp" && tlsUpgradeConfig.ServerName == "" {
// Use target-hostname as default for TLS-servername.
targetAddress, _, _ := net.SplitHostPort(target) // Had succeeded in dialTCP already.
tlsUpgradeConfig.ServerName = targetAddress
}

tlsConn := tls.Client(conn, tlsUpgradeConfig)
defer tlsConn.Close()

// Initiate TLS handshake (required here to get TLS state).
if err := tlsConn.Handshake(); err != nil {
logger.Error("TLS Handshake (client) failed", "err", err)
return false
}
logger.Debug("TLS Handshake (client) succeeded.")
conn = net.Conn(tlsConn)
scanner = bufio.NewScanner(conn)

// Get certificate expiry.
state := tlsConn.ConnectionState()
registry.MustRegister(probeSSLEarliestCertExpiry, probeTLSVersion, probeSSLLastChainExpiryTimestampSeconds, probeSSLLastInformation)
probeSSLEarliestCertExpiry.Set(float64(getEarliestCertExpiry(&state).Unix()))
probeTLSVersion.WithLabelValues(getTLSVersion(&state)).Set(1)
probeSSLLastChainExpiryTimestampSeconds.Set(float64(getLastChainExpiry(&state).Unix()))
probeSSLLastInformation.WithLabelValues(getFingerprint(&state), getSubject(&state), getIssuer(&state), getDNSNames(&state), getSerialNumber(&state)).Set(1)
}
}
return true
}
Loading