Repository: prometheus/blackbox_exporter
Pull request #538 (2019-10-09 19:47:26)

Export TLS version
This PR adds the TLS version to the exported variables, or NaN when it is unknown (maybe that should be 0 instead?). All currently available TLS versions are covered. SSL3 is not, as it is not TLS and will be removed in Go 1.14 (See https://github.com/golang/go/issues/32716).

SuperQ: Version information should be exposed as an info metric like this:

```
probe_tls_version_info{version="1.3"} 1
```

This avoids the problems with NaN in unknown.
silkeh: Right! I initially followed the format of `probe_http_version`. I have added two fixup commits that implement it as `probe_tls_version_info`.

Patch:
diff --git a/prober/http.go b/prober/http.go
index bd9722d..f4abdbe 100644
--- a/prober/http.go
+++ b/prober/http.go
@@ -261,6 +261,14 @@ func ProbeHTTP(ctx context.Context, target string, module config.Module, registr
 			Help: "Returns earliest SSL cert expiry in unixtime",
 		})
 
+		probeTLSVersion = prometheus.NewGaugeVec(
+			prometheus.GaugeOpts{
+				Name: "probe_tls_version_info",
+				Help: "Contains the TLS version used",
+			},
+			[]string{"version"},
+		)
+
 		probeHTTPVersionGauge = prometheus.NewGauge(prometheus.GaugeOpts{
 			Name: "probe_http_version",
 			Help: "Returns the version of HTTP of the probe response",
@@ -538,8 +546,9 @@ func ProbeHTTP(ctx context.Context, target string, module config.Module, registr
 
 	if resp.TLS != nil {
 		isSSLGauge.Set(float64(1))
-		registry.MustRegister(probeSSLEarliestCertExpiryGauge)
+		registry.MustRegister(probeSSLEarliestCertExpiryGauge, probeTLSVersion)
 		probeSSLEarliestCertExpiryGauge.Set(float64(getEarliestCertExpiry(resp.TLS).Unix()))
+		probeTLSVersion.WithLabelValues(getTLSVersion(resp.TLS)).Set(1)
 		if httpConfig.FailIfSSL {
 			level.Error(logger).Log("msg", "Final request was over SSL")
 			success = false
diff --git a/prober/tcp.go b/prober/tcp.go
index e497625..a450fd2 100644
--- a/prober/tcp.go
+++ b/prober/tcp.go
@@ -94,6 +94,13 @@ func ProbeTCP(ctx context.Context, target string, module config.Module, registry
 		Name: "probe_ssl_earliest_cert_expiry",
 		Help: "Returns earliest SSL cert expiry date",
 	})
+	probeTLSVersion := prometheus.NewGaugeVec(
+		prometheus.GaugeOpts{
+			Name: "probe_tls_version_info",
+			Help: "Returns the TLS version used, or NaN when unknown",
+		},
+		[]string{"version"},
+	)
 	probeFailedDueToRegex := prometheus.NewGauge(prometheus.GaugeOpts{
 		Name: "probe_failed_due_to_regex",
 		Help: "Indicates if probe failed due to regex",
@@ -118,8 +125,9 @@ func ProbeTCP(ctx context.Context, target string, module config.Module, registry
 	}
 	if module.TCP.TLS {
 		state := conn.(*tls.Conn).ConnectionState()
-		registry.MustRegister(probeSSLEarliestCertExpiry)
+		registry.MustRegister(probeSSLEarliestCertExpiry, probeTLSVersion)
 		probeSSLEarliestCertExpiry.Set(float64(getEarliestCertExpiry(&state).Unix()))
+		probeTLSVersion.WithLabelValues(getTLSVersion(&state)).Set(1)
 	}
 	scanner := bufio.NewScanner(conn)
 	for i, qr := range module.TCP.QueryResponse {
@@ -188,6 +196,7 @@ func ProbeTCP(ctx context.Context, target string, module config.Module, registry
 			state := tlsConn.ConnectionState()
 			registry.MustRegister(probeSSLEarliestCertExpiry)
 			probeSSLEarliestCertExpiry.Set(float64(getEarliestCertExpiry(&state).Unix()))
+			probeTLSVersion.WithLabelValues(getTLSVersion(&state)).Set(1)
 		}
 	}
 	return true
diff --git a/prober/tls.go b/prober/tls.go
index ea21dd1..758cbf3 100644
--- a/prober/tls.go
+++ b/prober/tls.go
@@ -27,3 +27,18 @@ func getEarliestCertExpiry(state *tls.ConnectionState) time.Time {
 	}
 	return earliest
 }
+
+func getTLSVersion(state *tls.ConnectionState) string {
+	switch state.Version {
+	case tls.VersionTLS10:
+		return "TLS 1.0"
+	case tls.VersionTLS11:
+		return "TLS 1.1"
+	case tls.VersionTLS12:
+		return "TLS 1.2"
+	case tls.VersionTLS13:
+		return "TLS 1.3"
+	default:
+		return "unknown"
+	}
+}


Test patch:
diff --git a/prober/tcp_test.go b/prober/tcp_test.go
index 8201300..e2fcbd4 100644
--- a/prober/tcp_test.go
+++ b/prober/tcp_test.go
@@ -118,6 +118,8 @@ func TestTCPConnectionWithTLS(t *testing.T) {
 		tlsConfig := &tls.Config{
 			ServerName:   "localhost",
 			Certificates: []tls.Certificate{testcert},
+			MinVersion:   tls.VersionTLS12,
+			MaxVersion:   tls.VersionTLS12,
 		}
 		tlsConn := tls.Server(conn, tlsConfig)
 		defer tlsConn.Close()
@@ -168,13 +170,24 @@ func TestTCPConnectionWithTLS(t *testing.T) {
 	}
 	<-ch
 
-	// Check the probe_ssl_earliest_cert_expiry.
+	// Check the resulting metrics.
 	mfs, err := registry.Gather()
 	if err != nil {
 		t.Fatal(err)
 	}
+
+	// Check labels
+	expectedLabels := map[string]map[string]string{
+		"probe_tls_version_info": {
+			"version": "TLS 1.2",
+		},
+	}
+	checkRegistryLabels(expectedLabels, mfs, t)
+
+	// Check values
 	expectedResults := map[string]float64{
 		"probe_ssl_earliest_cert_expiry": float64(certExpiry.Unix()),
+		"probe_tls_version_info":         1,
 	}
 	checkRegistryResults(expectedResults, mfs, t)
 }
diff --git a/prober/utils_test.go b/prober/utils_test.go
index 38b95ee..326da5d 100644
--- a/prober/utils_test.go
+++ b/prober/utils_test.go
@@ -45,6 +45,35 @@ func checkRegistryResults(expRes map[string]float64, mfs []*dto.MetricFamily, t
 	}
 }
 
+// Check if expected labels are in the registry
+func checkRegistryLabels(expRes map[string]map[string]string, mfs []*dto.MetricFamily, t *testing.T) {
+	results := make(map[string]map[string]string)
+	for _, mf := range mfs {
+		result := make(map[string]string)
+		for _, metric := range mf.Metric {
+			for _, l := range metric.GetLabel() {
+				result[l.GetName()] = l.GetValue()
+			}
+		}
+		results[mf.GetName()] = result
+	}
+
+	for metric, labelValues := range expRes {
+		if _, ok := results[metric]; !ok {
+			t.Fatalf("Expected metric %v not found in returned metrics", metric)
+		}
+		for name, exp := range labelValues {
+			val, ok := results[metric][name]
+			if !ok {
+				t.Fatalf("Expected label %v for metric %v not found in returned metrics", val, name)
+			}
+			if val != exp {
+				t.Fatalf("Expected: %v{%q=%q}, got: %v{%q=%q}", metric, name, exp, metric, name, val)
+			}
+		}
+	}
+}
+
 // Create test certificate with specified expiry date
 // Certificate will be self-signed and use localhost/127.0.0.1
 // Generated certificate and key are returned in PEM encoding