From 76678d21b5fee9127fe9b7d12bc8f5f1b20b698a Mon Sep 17 00:00:00 2001 From: anemyte Date: Sat, 11 Sep 2021 13:05:44 +0300 Subject: [PATCH] Add hostname parameter Signed-off-by: anemyte --- README.md | 27 +++++++++++++++++++++ main.go | 17 +++++++++++++ main_test.go | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++ prober/http.go | 17 ++++++++++--- 4 files changed, 124 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 95dee650..6658f7ef 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 d0a746fa..40082fc4 100644 --- a/main.go +++ b/main.go @@ -120,6 +120,23 @@ func probeHandler(w http.ResponseWriter, r *http.Request, c *config.Config, logg return } + if module.Prober == "http" { + paramHost := params.Get("hostname") + if paramHost != "" { + if module.HTTP.Headers == nil { + module.HTTP.Headers = make(map[string]string) + } else { + for name, value := range module.HTTP.Headers { + if strings.Title(name) == "Host" && value != paramHost { + http.Error(w, fmt.Sprintf("Host header defined both in module configuration (%s) and with parameter 'hostname' (%s)", value, paramHost), http.StatusBadRequest) + return + } + } + } + module.HTTP.Headers["Host"] = paramHost + } + } + sl := newScrapeLogger(logger, moduleName, target) level.Info(sl).Log("msg", "Beginning probe", "probe", module.Prober, "timeout_seconds", timeoutSeconds) diff --git a/main_test.go b/main_test.go index f9d536be..02b55970 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 95c53535..d88d5d2e 100644 --- a/prober/http.go +++ b/prober/http.go @@ -341,9 +341,20 @@ func ProbeHTTP(ctx context.Context, target string, module config.Module, registr httpClientConfig := module.HTTP.HTTPClientConfig if len(httpClientConfig.TLSConfig.ServerName) == 0 { - // If there is no `server_name` in tls_config, use - // the hostname of the target. - httpClientConfig.TLSConfig.ServerName = targetHost + // If there is no `server_name` in tls_config it makes + // sense to use the host header value to avoid possible + // TLS handshake problems. + changed := false + for name, value := range httpConfig.Headers { + if strings.Title(name) == "Host" { + httpClientConfig.TLSConfig.ServerName = value + changed = true + } + } + if !changed { + // Otherwise use the hostname of the target. + httpClientConfig.TLSConfig.ServerName = targetHost + } } client, err := pconfig.NewClientFromConfig(httpClientConfig, "http_probe", pconfig.WithKeepAlivesDisabled()) if err != nil {