Add hostname parameter (#823)
authorEvgeniy Y <44520858+anemyte@users.noreply.github.com>
Thu, 11 Nov 2021 00:35:41 +0000 (03:35 +0300)
committerGitHub <noreply@github.com>
Thu, 11 Nov 2021 00:35:41 +0000 (01:35 +0100)
* Add hostname parameter

Signed-off-by: anemyte <anemyte@gmail.com>
README.md
main.go
main_test.go
prober/http.go

index 95dee650618c3757182efde5edb0aacc26e78fc6..6658f7efcf8da4c68f02d402e1353b641c788482 100644 (file)
--- a/README.md
+++ b/README.md
@@ -101,6 +101,33 @@ scrape_configs:
         replacement: 127.0.0.1:9115  # The blackbox exporter's real hostname:port.
 ```
 
+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`:
+```yaml
+scrape_configs:
+  - job_name: blackbox_all
+    metrics_path: /probe
+    params:
+      module: [ http_2xx ]  # Look for a HTTP 200 response.
+    dns_sd_configs:
+      - names:
+          - example.com
+          - prometheus.io
+        type: A
+        port: 443
+    relabel_configs:
+      - source_labels: [__address__]
+        target_label: __param_target
+        replacement: https://$1/  # Make probe URL be like https://1.2.3.4:443/
+      - source_labels: [__param_target]
+        target_label: instance
+      - target_label: __address__
+        replacement: 127.0.0.1:9115  # The blackbox exporter's real hostname:port.
+      - source_labels: [__meta_dns_name]
+        target_label: __param_hostname  # Make domain name become 'Host' header for probe requests
+      - source_labels: [__meta_dns_name]
+        target_label: vhost  # and store it in 'vhost' label
+```
+
 ## Permissions
 
 The ICMP probe requires elevated privileges to function:
diff --git a/main.go b/main.go
index 971aed98f60a946a5679b1ff36f8579038c4f926..689ab8035fbdfd0564c5a6f4395860333ef2e50a 100644 (file)
--- a/main.go
+++ b/main.go
@@ -120,6 +120,15 @@ func probeHandler(w http.ResponseWriter, r *http.Request, c *config.Config, logg
                return
        }
 
+       hostname := params.Get("hostname")
+       if module.Prober == "http" && hostname != "" {
+               err = setHTTPHost(hostname, &module)
+               if err != nil {
+                       http.Error(w, err.Error(), http.StatusBadRequest)
+                       return
+               }
+       }
+
        sl := newScrapeLogger(logger, moduleName, target)
        level.Info(sl).Log("msg", "Beginning probe", "probe", module.Prober, "timeout_seconds", timeoutSeconds)
 
@@ -150,6 +159,23 @@ func probeHandler(w http.ResponseWriter, r *http.Request, c *config.Config, logg
        h.ServeHTTP(w, r)
 }
 
+func setHTTPHost(hostname string, module *config.Module) error {
+       // By creating a new hashmap and copying values there we
+       // ensure that the initial configuration remain intact.
+       headers := make(map[string]string)
+       if module.HTTP.Headers != nil {
+               for name, value := range module.HTTP.Headers {
+                       if strings.Title(name) == "Host" && value != hostname {
+                               return fmt.Errorf("host header defined both in module configuration (%s) and with URL-parameter 'hostname' (%s)", value, hostname)
+                       }
+                       headers[name] = value
+               }
+       }
+       headers["Host"] = hostname
+       module.HTTP.Headers = headers
+       return nil
+}
+
 type scrapeLogger struct {
        next         log.Logger
        buffer       bytes.Buffer
index ee2d6d8584fa4efa535d7278c7c1d1c83b9ca84e..e7ba5c68a276ede7eb53375dcc786d1cde8f5052 100644 (file)
@@ -15,6 +15,7 @@ package main
 
 import (
        "bytes"
+       "fmt"
        "net/http"
        "net/http/httptest"
        "strings"
@@ -190,3 +191,68 @@ func TestComputeExternalURL(t *testing.T) {
                }
        }
 }
+
+func TestHostnameParam(t *testing.T) {
+       headers := map[string]string{}
+       c := &config.Config{
+               Modules: map[string]config.Module{
+                       "http_2xx": config.Module{
+                               Prober:  "http",
+                               Timeout: 10 * time.Second,
+                               HTTP: config.HTTPProbe{
+                                       Headers:            headers,
+                                       IPProtocolFallback: true,
+                               },
+                       },
+               },
+       }
+
+       // check that 'hostname' parameter make its way to Host header
+       hostname := "foo.example.com"
+
+       ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+               if r.Host != hostname {
+                       t.Errorf("Unexpected Host: expected %q, got %q.", hostname, r.Host)
+               }
+               w.WriteHeader(http.StatusOK)
+       }))
+       defer ts.Close()
+
+       requrl := fmt.Sprintf("?debug=true&hostname=%s&target=%s", hostname, ts.URL)
+
+       req, err := http.NewRequest("GET", requrl, nil)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       rr := httptest.NewRecorder()
+
+       handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+               probeHandler(w, r, c, log.NewNopLogger(), &resultHistory{})
+       })
+
+       handler.ServeHTTP(rr, req)
+
+       if status := rr.Code; status != http.StatusOK {
+               t.Errorf("probe request handler returned wrong status code: %v, want %v", status, http.StatusOK)
+       }
+
+       // check that ts got the request to perform header check
+       if !strings.Contains(rr.Body.String(), "probe_success 1") {
+               t.Errorf("probe failed, response body: %v", rr.Body.String())
+       }
+
+       // check that host header both in config and in parameter will result in 400
+       c.Modules["http_2xx"].HTTP.Headers["Host"] = hostname + ".something"
+
+       handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+               probeHandler(w, r, c, log.NewNopLogger(), &resultHistory{})
+       })
+
+       rr = httptest.NewRecorder()
+       handler.ServeHTTP(rr, req)
+
+       if status := rr.Code; status != http.StatusBadRequest {
+               t.Errorf("probe request handler returned wrong status code: %v, want %v", status, http.StatusBadRequest)
+       }
+}
index fe7308270a9dac09d434ab74cefd3ef3cc552e1e..156feca76150548377270b3d0ca31418e46f7f13 100644 (file)
@@ -344,6 +344,15 @@ func ProbeHTTP(ctx context.Context, target string, module config.Module, registr
                // If there is no `server_name` in tls_config, use
                // the hostname of the target.
                httpClientConfig.TLSConfig.ServerName = targetHost
+
+               // However, if there is a Host header it is better to use
+               // its value instead. This helps avoid TLS handshake error
+               // if targetHost is an IP address.
+               for name, value := range httpConfig.Headers {
+                       if strings.Title(name) == "Host" {
+                               httpClientConfig.TLSConfig.ServerName = value
+                       }
+               }
        }
        client, err := pconfig.NewClientFromConfig(httpClientConfig, "http_probe", pconfig.WithKeepAlivesDisabled())
        if err != nil {