Repository: prometheus/blackbox_exporter
Pull request #927 (2022-06-13 23:23:42)

Make caser a local varialbe, not a global one.
The cases.Caser returned by calling cases.Title *cannot* be shared among
goroutines. This might happen when Prometheus tries to scrape multiple
targets at the same time. From the docs:

A Caser may be stateful and should therefore not be shared between
goroutines.

Fixes: #922

Signed-off-by: Marcelo E. Magallon <marcelo.magallon@grafana.com>


Patch:
diff --git a/CONFIGURATION.md b/CONFIGURATION.md
index 6de2238..35e8446 100644
--- a/CONFIGURATION.md
+++ b/CONFIGURATION.md
@@ -262,6 +262,11 @@ validate_additional_rrs:
 # The size of the payload.
 [ payload_size: <int> ]
 
+# TTL of outbound packets. Value must be in the range [0, 255]. Can be used
+# to test reachability of a target within a given number of hops, for example,
+# to determine when network routing has changed.
+[ ttl: <int> ]
+
 ```
 
 ### <grpc_probe>
diff --git a/blackbox.yml b/blackbox.yml
index 8418399..62884bc 100644
--- a/blackbox.yml
+++ b/blackbox.yml
@@ -42,3 +42,8 @@ modules:
       - expect: "^:[^ ]+ 001"
   icmp:
     prober: icmp
+  icmp_ttl5:
+    prober: icmp
+    timeout: 5s
+    icmp:
+      ttl: 5
\ No newline at end of file
diff --git a/config/config.go b/config/config.go
index 6737963..8094b29 100644
--- a/config/config.go
+++ b/config/config.go
@@ -77,8 +77,10 @@ var (
 	}
 
 	// DefaultICMPProbe set default value for ICMPProbe
+	DefaultICMPTTL   = 64
 	DefaultICMPProbe = ICMPProbe{
 		IPProtocolFallback: true,
+		TTL:                DefaultICMPTTL,
 	}
 
 	// DefaultDNSProbe set default value for DNSProbe
@@ -258,6 +260,7 @@ type ICMPProbe struct {
 	SourceIPAddress    string `yaml:"source_ip_address,omitempty"`
 	PayloadSize        int    `yaml:"payload_size,omitempty"`
 	DontFragment       bool   `yaml:"dont_fragment,omitempty"`
+	TTL                int    `yaml:"ttl,omitempty"`
 }
 
 type DNSProbe struct {
@@ -405,6 +408,13 @@ func (s *ICMPProbe) UnmarshalYAML(unmarshal func(interface{}) error) error {
 	if runtime.GOOS == "windows" && s.DontFragment {
 		return errors.New("\"dont_fragment\" is not supported on windows platforms")
 	}
+
+	if s.TTL < 0 {
+		return errors.New("\"ttl\" cannot be negative")
+	}
+	if s.TTL > 255 {
+		return errors.New("\"ttl\" cannot exceed 255")
+	}
 	return nil
 }
 
diff --git a/prober/http.go b/prober/http.go
index 54f4099..99d38a2 100644
--- a/prober/http.go
+++ b/prober/http.go
@@ -46,8 +46,6 @@ import (
 	"github.com/prometheus/blackbox_exporter/config"
 )
 
-var caser = cases.Title(language.Und)
-
 func matchRegularExpressions(reader io.Reader, httpConfig config.HTTPProbe, logger log.Logger) bool {
 	body, err := ioutil.ReadAll(reader)
 	if err != nil {
@@ -342,6 +340,16 @@ func ProbeHTTP(ctx context.Context, target string, module config.Module, registr
 		return false
 	}
 
+	// Do not move the following variable to global scope. The cases.Caser returned by
+	// calling cases.Title *cannot* be shared among goroutines. This might happen when
+	// Prometheus tries to scrape multiple targets at the same time. From the docs:
+	//
+	// A Caser may be stateful and should therefore not be shared between goroutines.
+	//
+	// Issue: https://github.com/prometheus/blackbox_exporter/issues/922
+
+	caser := cases.Title(language.Und)
+
 	httpClientConfig := module.HTTP.HTTPClientConfig
 	if len(httpClientConfig.TLSConfig.ServerName) == 0 {
 		// If there is no `server_name` in tls_config, use
diff --git a/prober/icmp.go b/prober/icmp.go
index aa6cad3..1f8432a 100644
--- a/prober/icmp.go
+++ b/prober/icmp.go
@@ -234,15 +234,31 @@ func ProbeICMP(ctx context.Context, target string, module config.Module, registr
 	rttStart := time.Now()
 
 	if icmpConn != nil {
+		ttl := module.ICMP.TTL
+		if ttl > 0 {
+			if c4 := icmpConn.IPv4PacketConn(); c4 != nil {
+				level.Debug(logger).Log("msg", "Setting TTL (IPv4 unprivileged)", "ttl", ttl)
+				c4.SetTTL(ttl)
+			}
+			if c6 := icmpConn.IPv6PacketConn(); c6 != nil {
+				level.Debug(logger).Log("msg", "Setting TTL (IPv6 unprivileged)", "ttl", ttl)
+				c6.SetHopLimit(ttl)
+			}
+		}
 		_, err = icmpConn.WriteTo(wb, dst)
 	} else {
+		ttl := config.DefaultICMPTTL
+		if module.ICMP.TTL > 0 {
+			level.Debug(logger).Log("msg", "Overriding TTL (raw IPv4)", "ttl", ttl)
+			ttl = module.ICMP.TTL
+		}
 		// Only for IPv4 raw. Needed for setting DontFragment flag.
 		header := &ipv4.Header{
 			Version:  ipv4.Version,
 			Len:      ipv4.HeaderLen,
 			Protocol: 1,
 			TotalLen: ipv4.HeaderLen + len(wb),
-			TTL:      64,
+			TTL:      ttl,
 			Dst:      dstIPAddr.IP,
 			Src:      srcIP,
 		}


Test patch:
diff --git a/config/config_test.go b/config/config_test.go
index 432d0d9..e1b57a3 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -87,6 +87,14 @@ func TestLoadBadConfigs(t *testing.T) {
 			input: "testdata/invalid-http-request-compression-reject-all-encodings.yml",
 			want:  `error parsing config file: invalid configuration "Accept-Encoding: *;q=0.0", "compression: gzip"`,
 		},
+		{
+			input: "testdata/invalid-icmp-ttl.yml",
+			want:  "error parsing config file: \"ttl\" cannot be negative",
+		},
+		{
+			input: "testdata/invalid-icmp-ttl-overflow.yml",
+			want:  "error parsing config file: \"ttl\" cannot exceed 255",
+		},
 		{
 			input: "testdata/invalid-tcp-query-response-regexp.yml",
 			want:  `error parsing config file: "Could not compile regular expression" regexp=":["`,
diff --git a/config/testdata/invalid-icmp-ttl-overflow.yml b/config/testdata/invalid-icmp-ttl-overflow.yml
new file mode 100644
index 0000000..9b3afb1
--- /dev/null
+++ b/config/testdata/invalid-icmp-ttl-overflow.yml
@@ -0,0 +1,5 @@
+modules:
+  icmp_test:
+    prober: icmp
+    icmp:
+      ttl: 256
diff --git a/config/testdata/invalid-icmp-ttl.yml b/config/testdata/invalid-icmp-ttl.yml
new file mode 100644
index 0000000..7eb4924
--- /dev/null
+++ b/config/testdata/invalid-icmp-ttl.yml
@@ -0,0 +1,5 @@
+modules:
+  icmp_test:
+    prober: icmp
+    icmp:
+      ttl: -1
diff --git a/prober/http_test.go b/prober/http_test.go
index dc5b65b..cd27761 100644
--- a/prober/http_test.go
+++ b/prober/http_test.go
@@ -35,6 +35,8 @@ import (
 	"github.com/go-kit/log"
 	"github.com/prometheus/client_golang/prometheus"
 	pconfig "github.com/prometheus/common/config"
+	"golang.org/x/text/cases"
+	"golang.org/x/text/language"
 
 	"github.com/prometheus/blackbox_exporter/config"
 )
@@ -1068,6 +1070,7 @@ func TestHTTPHeaders(t *testing.T) {
 		"Accept-Language": "en-US",
 	}
 	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		caser := cases.Title(language.Und)
 		for key, value := range headers {
 			if caser.String(key) == "Host" {
 				if r.Host != value {