From 6fcd1424e2e7943927b046dc4b1123ce69f32864 Mon Sep 17 00:00:00 2001 From: Evgeniy Y <44520858+anemyte@users.noreply.github.com> Date: Thu, 11 Nov 2021 03:35:41 +0300 Subject: [PATCH] Add hostname parameter (#823) * Add hostname parameter Signed-off-by: anemyte --- README.md | 27 +++++++++++++++++++++ main.go | 26 ++++++++++++++++++++ main_test.go | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++ prober/http.go | 9 +++++++ 4 files changed, 128 insertions(+) diff --git a/README.md b/README.md index 95dee65..6658f7e 100644 --- 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 971aed9..689ab80 100644 --- 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 diff --git a/main_test.go b/main_test.go index ee2d6d8..e7ba5c6 100644 --- a/main_test.go +++ b/main_test.go @@ -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) + } +} diff --git a/prober/http.go b/prober/http.go index fe73082..156feca 100644 --- a/prober/http.go +++ b/prober/http.go @@ -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 { -- 2.25.1