Skip to content

Commit

Permalink
Add hostname parameter
Browse files Browse the repository at this point in the history
Signed-off-by: anemyte <anemyte@gmail.com>
  • Loading branch information
anemyte committed Sep 11, 2021
1 parent 5bad55c commit 76678d2
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 3 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
17 changes: 17 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

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)
}
}
17 changes: 14 additions & 3 deletions prober/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 76678d2

Please sign in to comment.