diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 280991c9e..8d115978a 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -182,9 +182,18 @@ regexp: , [ source_ip_address: ] # The query sent in the TCP probe and the expected associated response. -# starttls upgrades TCP connection to TLS. +# "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 TCP connection to TLS. query_response: [ - [ [ expect: ], + [ labels: + - [ name: + value: + ], ... + ], [ send: ], [ starttls: ] ], ... diff --git a/blackbox.yml b/blackbox.yml index 1ad0c81a7..a091a4cd2 100644 --- a/blackbox.yml +++ b/blackbox.yml @@ -33,6 +33,17 @@ modules: query_response: - expect: "^SSH-2.0-" - send: "SSH-2.0-blackbox-ssh-check" + ssh_banner_extract: + prober: tcp + timeout: 5s + tcp: + query_response: + - expect: "^SSH-2.0-([^ -]+)(?: (.*))?$" + labels: + - name: ssh_version + value: "${1}" + - name: ssh_comments + value: "${2}" irc_banner: prober: tcp tcp: diff --git a/config/config.go b/config/config.go index d1a7dc46b..559dbf562 100644 --- a/config/config.go +++ b/config/config.go @@ -239,10 +239,16 @@ type HeaderMatch struct { AllowMissing bool `yaml:"allow_missing,omitempty"` } +type Label struct { + Name string `yaml:"name,omitempty"` + Value string `yaml:"value,omitempty"` +} + type QueryResponse struct { - Expect Regexp `yaml:"expect,omitempty"` - Send string `yaml:"send,omitempty"` - StartTLS bool `yaml:"starttls,omitempty"` + Expect Regexp `yaml:"expect,omitempty"` + Labels []Label `yaml:"labels,omitempty"` + Send string `yaml:"send,omitempty"` + StartTLS bool `yaml:"starttls,omitempty"` } type TCPProbe struct { diff --git a/prober/tcp.go b/prober/tcp.go index de960db26..47f68d700 100644 --- a/prober/tcp.go +++ b/prober/tcp.go @@ -88,6 +88,24 @@ func dialTCP(ctx context.Context, target string, module config.Module, registry return tls.DialWithDialer(dialer, dialProtocol, dialTarget, tlsConfig) } +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.Regexp.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 ProbeTCP(ctx context.Context, target string, module config.Module, registry *prometheus.Registry, logger log.Logger) bool { probeSSLEarliestCertExpiry := prometheus.NewGauge(sslEarliestCertExpiryGaugeOpts) probeSSLLastChainExpiryTimestampSeconds := prometheus.NewGauge(sslChainExpiryInTimeStampGaugeOpts) @@ -158,6 +176,9 @@ func ProbeTCP(ctx context.Context, target string, module config.Module, registry } probeFailedDueToRegex.Set(0) send = string(qr.Expect.Regexp.Expand(nil, []byte(send), scanner.Bytes(), match)) + if qr.Labels != nil { + probeExpectInfo(registry, &qr, scanner.Bytes(), match) + } } if send != "" { level.Debug(logger).Log("msg", "Sending line", "line", send) diff --git a/prober/tcp_test.go b/prober/tcp_test.go index 0deba3a83..5687267d1 100644 --- a/prober/tcp_test.go +++ b/prober/tcp_test.go @@ -525,8 +525,18 @@ func TestTCPConnectionQueryResponseMatching(t *testing.T) { IPProtocolFallback: true, QueryResponse: []config.QueryResponse{ { - Expect: config.MustNewRegexp("SSH-2.0-(OpenSSH_6.9p1) Debian-2"), + Expect: config.MustNewRegexp("^SSH-2.0-([^ -]+)(?: (.*))?$"), Send: "CONFIRM ${1}", + Labels: []config.Label{ + { + Name: "ssh_version", + Value: "${1}", + }, + { + Name: "ssh_comments", + Value: "${2}", + }, + }, }, }, }, @@ -560,6 +570,14 @@ func TestTCPConnectionQueryResponseMatching(t *testing.T) { "probe_failed_due_to_regex": 0, } checkRegistryResults(expectedResults, mfs, t) + // Check labels + expectedLabels := map[string]map[string]string{ + "probe_expect_info": { + "ssh_version": "OpenSSH_6.9p1", + "ssh_comments": "Debian-2", + }, + } + checkRegistryLabels(expectedLabels, mfs, t) } @@ -683,3 +701,38 @@ func TestPrometheusTimeoutTCP(t *testing.T) { } <-ch } + +func TestProbeExpectInfo(t *testing.T) { + registry := prometheus.NewRegistry() + qr := config.QueryResponse{ + Expect: config.MustNewRegexp("^SSH-2.0-([^ -]+)(?: (.*))?$"), + Labels: []config.Label{ + { + Name: "label1", + Value: "got ${1} here", + }, + { + Name: "label2", + Value: "${1} on ${2}", + }, + }, + } + bytes := []byte("SSH-2.0-OpenSSH_6.9p1 Debian-2") + match := qr.Expect.Regexp.FindSubmatchIndex(bytes) + + probeExpectInfo(registry, &qr, bytes, match) + + mfs, err := registry.Gather() + if err != nil { + t.Fatal(err) + } + // Check labels + expectedLabels := map[string]map[string]string{ + "probe_expect_info": { + "label1": "got OpenSSH_6.9p1 here", + "label2": "OpenSSH_6.9p1 on Debian-2", + }, + } + checkRegistryLabels(expectedLabels, mfs, t) + +}