Skip to content

Commit

Permalink
Add hostname parameter (#823)
Browse files Browse the repository at this point in the history
* Add hostname parameter

Signed-off-by: anemyte <[email protected]>
  • Loading branch information
anemyte authored Nov 11, 2021
1 parent 9a4c24a commit 6fcd142
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 0 deletions.
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
26 changes: 26 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
66 changes: 66 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package main

import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"strings"
Expand Down Expand Up @@ -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)
}
}
9 changes: 9 additions & 0 deletions prober/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 6fcd142

Please sign in to comment.